blob: 6c44a420f5cd502aa3857f51c9513a92f6667c25 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Gary Maib9065dd2016-11-08 10:49:00 -080019import static android.Manifest.permission.WRITE_CONTACTS;
20
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080021import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070022import android.app.IntentService;
23import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080024import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import android.content.ContentProviderResult;
26import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080027import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070028import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080029import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070030import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080031import android.content.OperationApplicationException;
32import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070033import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070034import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080035import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080036import android.os.Handler;
37import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080038import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070040import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080041import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080042import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080043import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080044import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070045import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080046import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070047import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070048import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070049import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070050import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070051import android.support.v4.os.ResultReceiver;
Marcus Hagerott7333c372016-11-07 09:40:20 -080052import android.telephony.SubscriptionInfo;
James Laskeyf62b4882016-10-21 11:36:40 -070053import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070054import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080055import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070056
Gary Mai363af602016-09-28 10:01:23 -070057import com.android.contacts.activities.ContactEditorActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070059import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070060import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080061import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080062import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070063import com.android.contacts.common.model.RawContactDelta;
64import com.android.contacts.common.model.RawContactDeltaList;
65import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott7333c372016-11-07 09:40:20 -080066import com.android.contacts.common.model.SimCard;
Marcus Hagerott819214d2016-09-29 14:58:27 -070067import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080068import com.android.contacts.common.model.account.AccountWithDataSet;
James Laskeyf62b4882016-10-21 11:36:40 -070069import com.android.contacts.common.preference.ContactsPreferences;
70import com.android.contacts.common.util.ContactDisplayUtils;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070071import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080072import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070073import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070074import com.android.contactsbind.FeedbackHelper;
Gary Maib9065dd2016-11-08 10:49:00 -080075
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070076import com.google.common.collect.Lists;
77import com.google.common.collect.Sets;
78
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080079import java.util.ArrayList;
Marcus Hagerott7333c372016-11-07 09:40:20 -080080import java.util.Collection;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080081import java.util.HashSet;
82import java.util.List;
83import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070084
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080085/**
86 * A service responsible for saving changes to the content provider.
87 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070088public class ContactSaveService extends IntentService {
89 private static final String TAG = "ContactSaveService";
90
Katherine Kuana007e442011-07-07 09:25:34 -070091 /** Set to true in order to view logs on content provider operations */
92 private static final boolean DEBUG = false;
93
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070094 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
95
96 public static final String EXTRA_ACCOUNT_NAME = "accountName";
97 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070098 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070099 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700100 public static final String EXTRA_CONTENT_VALUES = "contentValues";
101 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -0700102 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
103 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700104
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800105 public static final String ACTION_SAVE_CONTACT = "saveContact";
106 public static final String EXTRA_CONTACT_STATE = "state";
107 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700108 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700109 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800110 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700111
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800112 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800113 public static final String ACTION_RENAME_GROUP = "renameGroup";
114 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700115 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800116 public static final String EXTRA_GROUP_ID = "groupId";
117 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700118 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
119 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800120
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800121 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800122 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800123 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800124 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800125 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800126 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700127 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700128 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800129
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800130 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
131 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
132 public static final String EXTRA_DATA_ID = "dataId";
133
Gary Mai7efa9942016-05-12 11:26:49 -0700134 public static final String ACTION_SPLIT_CONTACT = "splitContact";
Gary Maib9065dd2016-11-08 10:49:00 -0800135 public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
Gary Mai7efa9942016-05-12 11:26:49 -0700136
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800137 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800138 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800139 public static final String EXTRA_CONTACT_ID1 = "contactId1";
140 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800141
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700142 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
143 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
144
145 public static final String ACTION_SET_RINGTONE = "setRingtone";
146 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
147
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700148 public static final String ACTION_UNDO = "undo";
149 public static final String EXTRA_UNDO_ACTION = "undoAction";
150 public static final String EXTRA_UNDO_DATA = "undoData";
151
Marcus Hagerott819214d2016-09-29 14:58:27 -0700152 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
153 public static final String EXTRA_SIM_CONTACTS = "simContacts";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800154 public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
155
156 // For debugging and testing what happens when requests are queued up.
157 public static final String ACTION_SLEEP = "sleep";
158 public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700159
160 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
161 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
Gary Maib9065dd2016-11-08 10:49:00 -0800162 public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
163 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800164
165 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700166
167 public static final String EXTRA_RESULT_CODE = "resultCode";
168 public static final String EXTRA_RESULT_COUNT = "count";
169 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700170
Gary Mai7efa9942016-05-12 11:26:49 -0700171 public static final int CP2_ERROR = 0;
172 public static final int CONTACTS_LINKED = 1;
173 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700174 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700175 public static final int RESULT_UNKNOWN = 0;
176 public static final int RESULT_SUCCESS = 1;
177 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700178
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700179 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
180 Data.MIMETYPE,
181 Data.IS_PRIMARY,
182 Data.DATA1,
183 Data.DATA2,
184 Data.DATA3,
185 Data.DATA4,
186 Data.DATA5,
187 Data.DATA6,
188 Data.DATA7,
189 Data.DATA8,
190 Data.DATA9,
191 Data.DATA10,
192 Data.DATA11,
193 Data.DATA12,
194 Data.DATA13,
195 Data.DATA14,
196 Data.DATA15
197 );
198
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800199 private static final int PERSIST_TRIES = 3;
200
Walter Jang0653de32015-07-24 12:12:40 -0700201 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
202
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800203 public interface Listener {
204 public void onServiceCompleted(Intent callbackIntent);
205 }
206
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100207 private static final CopyOnWriteArrayList<Listener> sListeners =
208 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800209
Marcus Hagerott7333c372016-11-07 09:40:20 -0800210 // Holds the current state of the service
211 private static final State sState = new State();
212
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800213 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700214 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700215 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800216
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700217 public ContactSaveService() {
218 super(TAG);
219 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800220 mMainHandler = new Handler(Looper.getMainLooper());
221 }
222
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700223 @Override
224 public void onCreate() {
225 super.onCreate();
226 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700227 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700228 }
229
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800230 public static void registerListener(Listener listener) {
231 if (!(listener instanceof Activity)) {
232 throw new ClassCastException("Only activities can be registered to"
233 + " receive callback from " + ContactSaveService.class.getName());
234 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100235 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800236 }
237
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700238 public static boolean canUndo(Intent resultIntent) {
239 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
240 }
241
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800242 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100243 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700244 }
245
Marcus Hagerott7333c372016-11-07 09:40:20 -0800246 public static State getState() {
247 return sState;
248 }
249
250 private void notifyStateChanged() {
251 LocalBroadcastManager.getInstance(this)
252 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
253 }
254
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800255 /**
256 * Returns true if the ContactSaveService was started successfully and false if an exception
257 * was thrown and a Toast error message was displayed.
258 */
259 public static boolean startService(Context context, Intent intent, int saveMode) {
260 try {
261 context.startService(intent);
262 } catch (Exception exception) {
263 final int resId;
264 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700265 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800266 resId = R.string.contactUnlinkErrorToast;
267 break;
Gary Mai363af602016-09-28 10:01:23 -0700268 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800269 resId = R.string.contactJoinErrorToast;
270 break;
Gary Mai363af602016-09-28 10:01:23 -0700271 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800272 resId = R.string.contactSavedErrorToast;
273 break;
274 default:
275 resId = R.string.contactGenericErrorToast;
276 }
277 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
278 return false;
279 }
280 return true;
281 }
282
283 /**
284 * Utility method that starts service and handles exception.
285 */
286 public static void startService(Context context, Intent intent) {
287 try {
288 context.startService(intent);
289 } catch (Exception exception) {
290 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
291 }
292 }
293
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700294 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295 public Object getSystemService(String name) {
296 Object service = super.getSystemService(name);
297 if (service != null) {
298 return service;
299 }
300
301 return getApplicationContext().getSystemService(name);
302 }
303
Marcus Hagerott7333c372016-11-07 09:40:20 -0800304 // Parent classes Javadoc says not to override this method but we're doing it just to update
305 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800306 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800307 public int onStartCommand(Intent intent, int flags, int startId) {
308 sState.onStart(intent);
309 notifyStateChanged();
310 return super.onStartCommand(intent, flags, startId);
311 }
312
313 @Override
314 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800315 if (intent == null) {
316 Log.d(TAG, "onHandleIntent: could not handle null intent");
317 return;
318 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700319 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
320 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
321 // TODO: add more specific error string such as "Turn on Contacts
322 // permission to update your contacts"
323 showToast(R.string.contactSavedErrorToast);
324 return;
325 }
326
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700327 // Call an appropriate method. If we're sure it affects how incoming phone calls are
328 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700329 String action = intent.getAction();
330 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
331 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800332 } else if (ACTION_SAVE_CONTACT.equals(action)) {
333 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800334 } else if (ACTION_CREATE_GROUP.equals(action)) {
335 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800336 } else if (ACTION_RENAME_GROUP.equals(action)) {
337 renameGroup(intent);
338 } else if (ACTION_DELETE_GROUP.equals(action)) {
339 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700340 } else if (ACTION_UPDATE_GROUP.equals(action)) {
341 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800342 } else if (ACTION_SET_STARRED.equals(action)) {
343 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800344 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
345 setSuperPrimary(intent);
346 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
347 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800348 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
349 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800350 } else if (ACTION_DELETE_CONTACT.equals(action)) {
351 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700352 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
353 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800354 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
355 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800356 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
357 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700358 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
359 setSendToVoicemail(intent);
360 } else if (ACTION_SET_RINGTONE.equals(action)) {
361 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700362 } else if (ACTION_UNDO.equals(action)) {
363 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700364 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
365 importFromSim(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800366 } else if (ACTION_SLEEP.equals(action)) {
367 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700368 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800369
370 sState.onFinish(intent);
371 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700372 }
373
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800374 /**
375 * Creates an intent that can be sent to this service to create a new raw contact
376 * using data presented as a set of ContentValues.
377 */
378 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700379 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700380 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800381 Intent serviceIntent = new Intent(
382 context, ContactSaveService.class);
383 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
384 if (account != null) {
385 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
386 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700387 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800388 }
389 serviceIntent.putParcelableArrayListExtra(
390 ContactSaveService.EXTRA_CONTENT_VALUES, values);
391
392 // Callback intent will be invoked by the service once the new contact is
393 // created. The service will put the URI of the new contact as "data" on
394 // the callback intent.
395 Intent callbackIntent = new Intent(context, callbackActivity);
396 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800397 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
398 return serviceIntent;
399 }
400
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700401 private void createRawContact(Intent intent) {
402 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
403 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700404 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700405 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
406 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
407
408 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
409 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
410 .withValue(RawContacts.ACCOUNT_NAME, accountName)
411 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700412 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700413 .build());
414
415 int size = valueList.size();
416 for (int i = 0; i < size; i++) {
417 ContentValues values = valueList.get(i);
418 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
419 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
420 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
421 .withValues(values)
422 .build());
423 }
424
425 ContentResolver resolver = getContentResolver();
426 ContentProviderResult[] results;
427 try {
428 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
429 } catch (Exception e) {
430 throw new RuntimeException("Failed to store new contact", e);
431 }
432
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700433 Uri rawContactUri = results[0].uri;
434 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
435
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800436 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700437 }
438
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700439 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440 * Creates an intent that can be sent to this service to create a new raw contact
441 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800442 * This variant is more convenient to use when there is only one photo that can
443 * possibly be updated, as in the Contact Details screen.
444 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
445 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800446 */
Maurice Chu851222a2012-06-21 11:43:08 -0700447 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700448 String saveModeExtraKey, int saveMode, boolean isProfile,
449 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700450 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800451 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700452 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800453 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700454 callbackActivity, callbackAction, bundle,
455 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800456 }
457
458 /**
459 * Creates an intent that can be sent to this service to create a new raw contact
460 * using data presented as a set of ContentValues.
461 * This variant is used when multiple contacts' photos may be updated, as in the
462 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700463 *
Josh Garguse692e012012-01-18 14:53:11 -0800464 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700465 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
466 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800467 */
Maurice Chu851222a2012-06-21 11:43:08 -0700468 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700469 String saveModeExtraKey, int saveMode, boolean isProfile,
470 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700471 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000472 Intent serviceIntent = new Intent(
473 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800474 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
475 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700476 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800477 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
478
Josh Garguse692e012012-01-18 14:53:11 -0800479 if (updatedPhotos != null) {
480 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
481 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800482
Josh Garguse5d3f892012-04-11 11:56:15 -0700483 if (callbackActivity != null) {
484 // Callback intent will be invoked by the service once the contact is
485 // saved. The service will put the URI of the new contact as "data" on
486 // the callback intent.
487 Intent callbackIntent = new Intent(context, callbackActivity);
488 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700489 if (joinContactIdExtraKey != null && joinContactId != null) {
490 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
491 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700492 callbackIntent.setAction(callbackAction);
493 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
494 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800495 return serviceIntent;
496 }
497
498 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700499 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700500 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800501 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800502
Jay Shrauner08099782015-03-25 14:17:11 -0700503 if (state == null) {
504 Log.e(TAG, "Invalid arguments for saveContact request");
505 return;
506 }
507
benny.lin3a4e7a22014-01-08 10:58:08 +0800508 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800509 // Trim any empty fields, and RawContacts, before persisting
510 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700511 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512
513 Uri lookupUri = null;
514
515 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700516
Josh Garguse692e012012-01-18 14:53:11 -0800517 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800518
Josh Gargusef15c8e2012-01-30 16:42:02 -0800519 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
520 long insertedRawContactId = -1;
521
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800522 // Attempt to persist changes
523 int tries = 0;
524 while (tries++ < PERSIST_TRIES) {
525 try {
526 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800527 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
528
529 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
530
531 for (CPOWrapper cpoWrapper : diffWrapper) {
532 diff.add(cpoWrapper.getOperation());
533 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700534
Katherine Kuana007e442011-07-07 09:25:34 -0700535 if (DEBUG) {
536 Log.v(TAG, "Content Provider Operations:");
537 for (ContentProviderOperation operation : diff) {
538 Log.v(TAG, operation.toString());
539 }
540 }
541
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700542 int numberProcessed = 0;
543 boolean batchFailed = false;
544 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
545 while (numberProcessed < diff.size()) {
546 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
547 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700548 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700549 batchFailed = true;
550 break;
551 } else {
552 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700553 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800554 }
555
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700556 if (batchFailed) {
557 // Retry save
558 continue;
559 }
560
Wenyi Wang67addcc2015-11-23 10:07:48 -0800561 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800562 if (rawContactId == -1) {
563 throw new IllegalStateException("Could not determine RawContact ID after save");
564 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800565 // We don't have to check to see if the value is still -1. If we reach here,
566 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800567 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700568 if (isProfile) {
569 // Since the profile supports local raw contacts, which may have been completely
570 // removed if all information was removed, we need to do a special query to
571 // get the lookup URI for the profile contact (if it still exists).
572 Cursor c = resolver.query(Profile.CONTENT_URI,
573 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
574 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800575 if (c == null) {
576 continue;
577 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700578 try {
Erik162b7e32011-09-20 15:23:55 -0700579 if (c.moveToFirst()) {
580 final long contactId = c.getLong(0);
581 final String lookupKey = c.getString(1);
582 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
583 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700584 } finally {
585 c.close();
586 }
587 } else {
588 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
589 rawContactId);
590 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
591 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800592 if (lookupUri != null) {
593 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
594 }
Josh Garguse692e012012-01-18 14:53:11 -0800595
596 // We can change this back to false later, if we fail to save the contact photo.
597 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800598 break;
599
600 } catch (RemoteException e) {
601 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700602 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800603 break;
604
Jay Shrauner57fca182014-01-17 14:20:50 -0800605 } catch (IllegalArgumentException e) {
606 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700607 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800608 showToast(R.string.contactSavedErrorToast);
609 break;
610
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800611 } catch (OperationApplicationException e) {
612 // Version consistency failed, re-parent change and try again
613 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
614 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
615 boolean first = true;
616 final int count = state.size();
617 for (int i = 0; i < count; i++) {
618 Long rawContactId = state.getRawContactId(i);
619 if (rawContactId != null && rawContactId != -1) {
620 if (!first) {
621 sb.append(',');
622 }
623 sb.append(rawContactId);
624 first = false;
625 }
626 }
627 sb.append(")");
628
629 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800630 throw new IllegalStateException(
631 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800632 }
633
Maurice Chu851222a2012-06-21 11:43:08 -0700634 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700635 isProfile
636 ? RawContactsEntity.PROFILE_CONTENT_URI
637 : RawContactsEntity.CONTENT_URI,
638 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700639 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700640
641 // Update the new state to use profile URIs if appropriate.
642 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700643 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700644 delta.setProfileQueryUri();
645 }
646 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800647 }
648 }
649
Josh Garguse692e012012-01-18 14:53:11 -0800650 // Now save any updated photos. We do this at the end to ensure that
651 // the ContactProvider already knows about newly-created contacts.
652 if (updatedPhotos != null) {
653 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700654 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800655 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800656
657 // If the raw-contact ID is negative, we are saving a new raw-contact;
658 // replace the bogus ID with the new one that we actually saved the contact at.
659 if (rawContactId < 0) {
660 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800661 }
662
Jay Shrauner511561d2015-04-02 10:35:33 -0700663 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700664 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700665 succeeded = false;
666 }
Josh Garguse692e012012-01-18 14:53:11 -0800667 }
668 }
669
Josh Garguse5d3f892012-04-11 11:56:15 -0700670 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
671 if (callbackIntent != null) {
672 if (succeeded) {
673 // Mark the intent to indicate that the save was successful (even if the lookup URI
674 // is now null). For local contacts or the local profile, it's possible that the
675 // save triggered removal of the contact, so no lookup URI would exist..
676 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
677 }
678 callbackIntent.setData(lookupUri);
679 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800680 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800681 }
682
Josh Garguse692e012012-01-18 14:53:11 -0800683 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700684 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
685 * subsets, adds the returned array to "results".
686 *
687 * @return the size of the array, if not null; -1 when the array is null.
688 */
689 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
690 ContentProviderResult[] results, ContentResolver resolver)
691 throws RemoteException, OperationApplicationException {
692 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
693 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
694 subset.addAll(diff.subList(offset, offset + subsetCount));
695 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
696 .AUTHORITY, subset);
697 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
698 return -1;
699 }
700 for (ContentProviderResult c : subsetResult) {
701 results[offset++] = c;
702 }
703 return subsetResult.length;
704 }
705
706 /**
Josh Garguse692e012012-01-18 14:53:11 -0800707 * Save updated photo for the specified raw-contact.
708 * @return true for success, false for failure
709 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800710 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800711 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800712 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
713 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
714
benny.lin3a4e7a22014-01-08 10:58:08 +0800715 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800716 }
717
Josh Gargusef15c8e2012-01-30 16:42:02 -0800718 /**
719 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
720 */
Maurice Chu851222a2012-06-21 11:43:08 -0700721 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800722 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800723 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800724 long existingRawContactId = state.findRawContactId();
725 if (existingRawContactId != -1) {
726 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800727 }
728
Wenyi Wang67addcc2015-11-23 10:07:48 -0800729 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800730 }
731
732 /**
733 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
734 */
735 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800736 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800737 if (results == null) {
738 return -1;
739 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800740 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800741 final int numResults = results.length;
742 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800743 final CPOWrapper cpoWrapper = diffWrapper.get(i);
744 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
745 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
746 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800747 return ContentUris.parseId(results[i].uri);
748 }
749 }
750 return -1;
751 }
752
753 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 * Creates an intent that can be sent to this service to create a new group as
755 * well as add new members at the same time.
756 *
757 * @param context of the application
758 * @param account in which the group should be created
759 * @param label is the name of the group (cannot be null)
760 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
761 * should be added to the group
762 * @param callbackActivity is the activity to send the callback intent to
763 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700764 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700765 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700766 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700767 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800768 Intent serviceIntent = new Intent(context, ContactSaveService.class);
769 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
770 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
771 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700772 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800773 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700774 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700775
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800776 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700777 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800778 Intent callbackIntent = new Intent(context, callbackActivity);
779 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700780 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800781
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700782 return serviceIntent;
783 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800784
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800785 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700786 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
787 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
788 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
789 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700790 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800791
Katherine Kuan717e3432011-07-13 17:03:24 -0700792 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700793 final Uri groupUri = mGroupsDao.create(label,
794 new AccountWithDataSet(accountName, accountType, dataSet));
795 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700796
797 // If there's no URI, then the insertion failed. Abort early because group members can't be
798 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800799 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700800 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800801 return;
802 }
803
Katherine Kuan717e3432011-07-13 17:03:24 -0700804 // Add new group members
805 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
806
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700807 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700808 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700809 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700810 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800811 values.clear();
812 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
813 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
814
815 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700816 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700817 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800818 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800819 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800820 }
821
822 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800823 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800824 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700825 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700826 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800827 Intent serviceIntent = new Intent(context, ContactSaveService.class);
828 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
829 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
830 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700831
832 // Callback intent will be invoked by the service once the group is renamed.
833 Intent callbackIntent = new Intent(context, callbackActivity);
834 callbackIntent.setAction(callbackAction);
835 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
836
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800837 return serviceIntent;
838 }
839
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 private void renameGroup(Intent intent) {
841 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
842 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
843
844 if (groupId == -1) {
845 Log.e(TAG, "Invalid arguments for renameGroup request");
846 return;
847 }
848
849 ContentValues values = new ContentValues();
850 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700851 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
852 getContentResolver().update(groupUri, values, null, null);
853
854 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
855 callbackIntent.setData(groupUri);
856 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800857 }
858
859 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800860 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800861 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700862 public static Intent createGroupDeletionIntent(Context context, long groupId) {
863 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800864 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800865 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700866
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800867 return serviceIntent;
868 }
869
870 private void deleteGroup(Intent intent) {
871 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
872 if (groupId == -1) {
873 Log.e(TAG, "Invalid arguments for deleteGroup request");
874 return;
875 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700876 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800877
Marcus Hagerott819214d2016-09-29 14:58:27 -0700878 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700879 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
880 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
881 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700882
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700883 mGroupsDao.delete(groupUri);
884
885 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
886 }
887
888 public static Intent createUndoIntent(Context context, Intent resultIntent) {
889 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
890 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
891 serviceIntent.putExtras(resultIntent);
892 return serviceIntent;
893 }
894
895 private void undo(Intent intent) {
896 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
897 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
898 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700899 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800900 }
901
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700902
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800903 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700904 * Creates an intent that can be sent to this service to rename a group as
905 * well as add and remove members from the group.
906 *
907 * @param context of the application
908 * @param groupId of the group that should be modified
909 * @param newLabel is the updated name of the group (can be null if the name
910 * should not be updated)
911 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
912 * should be added to the group
913 * @param rawContactsToRemove is an array of raw contact IDs for contacts
914 * that should be removed from the group
915 * @param callbackActivity is the activity to send the callback intent to
916 * @param callbackAction is the intent action for the callback intent
917 */
918 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
919 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700920 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700921 Intent serviceIntent = new Intent(context, ContactSaveService.class);
922 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
925 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
926 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
927 rawContactsToRemove);
928
929 // Callback intent will be invoked by the service once the group is updated
930 Intent callbackIntent = new Intent(context, callbackActivity);
931 callbackIntent.setAction(callbackAction);
932 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
933
934 return serviceIntent;
935 }
936
937 private void updateGroup(Intent intent) {
938 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
939 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
940 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
941 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
942
943 if (groupId == -1) {
944 Log.e(TAG, "Invalid arguments for updateGroup request");
945 return;
946 }
947
948 final ContentResolver resolver = getContentResolver();
949 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
950
951 // Update group name if necessary
952 if (label != null) {
953 ContentValues values = new ContentValues();
954 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700955 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700956 }
957
Katherine Kuan717e3432011-07-13 17:03:24 -0700958 // Add and remove members if necessary
959 addMembersToGroup(resolver, rawContactsToAdd, groupId);
960 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
961
962 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
963 callbackIntent.setData(groupUri);
964 deliverCallback(callbackIntent);
965 }
966
Walter Jang3a0b4832016-10-12 11:02:54 -0700967 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700968 long groupId) {
969 if (rawContactsToAdd == null) {
970 return;
971 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700972 for (long rawContactId : rawContactsToAdd) {
973 try {
974 final ArrayList<ContentProviderOperation> rawContactOperations =
975 new ArrayList<ContentProviderOperation>();
976
977 // Build an assert operation to ensure the contact is not already in the group
978 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
979 .newAssertQuery(Data.CONTENT_URI);
980 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
981 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
982 new String[] { String.valueOf(rawContactId),
983 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
984 assertBuilder.withExpectedCount(0);
985 rawContactOperations.add(assertBuilder.build());
986
987 // Build an insert operation to add the contact to the group
988 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
989 .newInsert(Data.CONTENT_URI);
990 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
991 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
992 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
993 rawContactOperations.add(insertBuilder.build());
994
995 if (DEBUG) {
996 for (ContentProviderOperation operation : rawContactOperations) {
997 Log.v(TAG, operation.toString());
998 }
999 }
1000
1001 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001002 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -08001003 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001004 }
1005 } catch (RemoteException e) {
1006 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -07001007 FeedbackHelper.sendFeedback(this, TAG,
1008 "Problem persisting user edits for raw contact ID " +
1009 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001010 } catch (OperationApplicationException e) {
1011 // The assert could have failed because the contact is already in the group,
1012 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001013 FeedbackHelper.sendFeedback(this, TAG,
1014 "Assert failed in adding raw contact ID " +
1015 String.valueOf(rawContactId) + ". Already exists in group " +
1016 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001017 }
1018 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001019 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001020
Daniel Lehmann18958a22012-02-28 17:45:25 -08001021 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001022 long groupId) {
1023 if (rawContactsToRemove == null) {
1024 return;
1025 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001026 for (long rawContactId : rawContactsToRemove) {
1027 // Apply the delete operation on the data row for the given raw contact's
1028 // membership in the given group. If no contact matches the provided selection, then
1029 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001030 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001031 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1032 new String[] { String.valueOf(rawContactId),
1033 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1034 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001035 }
1036
1037 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001038 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001039 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001040 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1041 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1042 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1043 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1044 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1045
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001046 return serviceIntent;
1047 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001048
1049 private void setStarred(Intent intent) {
1050 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1051 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1052 if (contactUri == null) {
1053 Log.e(TAG, "Invalid arguments for setStarred request");
1054 return;
1055 }
1056
1057 final ContentValues values = new ContentValues(1);
1058 values.put(Contacts.STARRED, value);
1059 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001060
1061 // Undemote the contact if necessary
1062 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1063 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001064 if (c == null) {
1065 return;
1066 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001067 try {
1068 if (c.moveToFirst()) {
1069 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001070
1071 // Don't bother undemoting if this contact is the user's profile.
1072 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001073 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001074 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001075 }
1076 } finally {
1077 c.close();
1078 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001079 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001080
1081 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001082 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1083 */
1084 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1085 boolean value) {
1086 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1087 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1088 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1089 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1090
1091 return serviceIntent;
1092 }
1093
1094 private void setSendToVoicemail(Intent intent) {
1095 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1096 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1097 if (contactUri == null) {
1098 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1099 return;
1100 }
1101
1102 final ContentValues values = new ContentValues(1);
1103 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1104 getContentResolver().update(contactUri, values, null, null);
1105 }
1106
1107 /**
1108 * Creates an intent that can be sent to this service to save the contact's ringtone.
1109 */
1110 public static Intent createSetRingtone(Context context, Uri contactUri,
1111 String value) {
1112 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1113 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1114 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1115 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1116
1117 return serviceIntent;
1118 }
1119
1120 private void setRingtone(Intent intent) {
1121 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1122 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1123 if (contactUri == null) {
1124 Log.e(TAG, "Invalid arguments for setRingtone");
1125 return;
1126 }
1127 ContentValues values = new ContentValues(1);
1128 values.put(Contacts.CUSTOM_RINGTONE, value);
1129 getContentResolver().update(contactUri, values, null, null);
1130 }
1131
1132 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001133 * Creates an intent that sets the selected data item as super primary (default)
1134 */
1135 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1136 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1137 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1138 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1139 return serviceIntent;
1140 }
1141
1142 private void setSuperPrimary(Intent intent) {
1143 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1144 if (dataId == -1) {
1145 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1146 return;
1147 }
1148
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001149 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001150 }
1151
1152 /**
1153 * Creates an intent that clears the primary flag of all data items that belong to the same
1154 * raw_contact as the given data item. Will only clear, if the data item was primary before
1155 * this call
1156 */
1157 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1158 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1159 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1160 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1161 return serviceIntent;
1162 }
1163
1164 private void clearPrimary(Intent intent) {
1165 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1166 if (dataId == -1) {
1167 Log.e(TAG, "Invalid arguments for clearPrimary request");
1168 return;
1169 }
1170
1171 // Update the primary values in the data record.
1172 ContentValues values = new ContentValues(1);
1173 values.put(Data.IS_SUPER_PRIMARY, 0);
1174 values.put(Data.IS_PRIMARY, 0);
1175
1176 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1177 values, null, null);
1178 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001179
1180 /**
1181 * Creates an intent that can be sent to this service to delete a contact.
1182 */
1183 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1184 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1185 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1186 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1187 return serviceIntent;
1188 }
1189
Brian Attwelld2962a32015-03-02 14:48:50 -08001190 /**
1191 * Creates an intent that can be sent to this service to delete multiple contacts.
1192 */
1193 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001194 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001195 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1196 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1197 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001198 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001199 return serviceIntent;
1200 }
1201
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001202 private void deleteContact(Intent intent) {
1203 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1204 if (contactUri == null) {
1205 Log.e(TAG, "Invalid arguments for deleteContact request");
1206 return;
1207 }
1208
1209 getContentResolver().delete(contactUri, null, null);
1210 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001211
Brian Attwelld2962a32015-03-02 14:48:50 -08001212 private void deleteMultipleContacts(Intent intent) {
1213 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1214 if (contactIds == null) {
1215 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1216 return;
1217 }
1218 for (long contactId : contactIds) {
1219 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1220 getContentResolver().delete(contactUri, null, null);
1221 }
James Laskeye5a140a2016-10-18 15:43:42 -07001222 final String[] names = intent.getStringArrayExtra(
1223 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1224 final String deleteToastMessage;
James Laskey56019ad2016-11-14 16:38:35 -08001225 if (contactIds.length != names.length || names.length == 0) {
James Laskeye5a140a2016-10-18 15:43:42 -07001226 deleteToastMessage = getResources().getQuantityString(
1227 R.plurals.contacts_deleted_toast, contactIds.length);
1228 } else if (names.length == 1) {
1229 deleteToastMessage = getResources().getString(
1230 R.string.contacts_deleted_one_named_toast, names);
1231 } else if (names.length == 2) {
1232 deleteToastMessage = getResources().getString(
1233 R.string.contacts_deleted_two_named_toast, names);
1234 } else {
1235 deleteToastMessage = getResources().getString(
1236 R.string.contacts_deleted_many_named_toast, names);
1237 }
James Laskey56019ad2016-11-14 16:38:35 -08001238
Wenyi Wang687d2182015-10-28 17:03:18 -07001239 mMainHandler.post(new Runnable() {
1240 @Override
1241 public void run() {
1242 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1243 .show();
1244 }
1245 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001246 }
1247
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001248 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001249 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Maib9065dd2016-11-08 10:49:00 -08001250 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001251 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001252 */
1253 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1254 ResultReceiver receiver) {
1255 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1256 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1257 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1258 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1259 return serviceIntent;
1260 }
1261
Gary Maib9065dd2016-11-08 10:49:00 -08001262 /**
1263 * Creates an intent that can be sent to this service to split a contact into it's constituent
1264 * pieces. This will explicitly set the raw contact ids to
1265 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1266 */
1267 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1268 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1269 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1270 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1271 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1272 return serviceIntent;
1273 }
1274
Gary Mai7efa9942016-05-12 11:26:49 -07001275 private void splitContact(Intent intent) {
1276 final long rawContactIds[][] = (long[][]) intent
1277 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001278 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001279 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001280 if (rawContactIds == null) {
1281 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001282 if (receiver != null) {
1283 receiver.send(BAD_ARGUMENTS, new Bundle());
1284 }
Gary Mai7efa9942016-05-12 11:26:49 -07001285 return;
1286 }
1287 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1288 final ContentResolver resolver = getContentResolver();
1289 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001290 for (int i = 0; i < rawContactIds.length; i++) {
1291 for (int j = 0; j < rawContactIds.length; j++) {
1292 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001293 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1294 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001295 if (receiver != null) {
1296 receiver.send(CP2_ERROR, new Bundle());
1297 return;
1298 }
1299 }
1300 }
1301 }
1302 }
1303 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1304 if (receiver != null) {
1305 receiver.send(CP2_ERROR, new Bundle());
1306 }
1307 return;
1308 }
Gary Maib9065dd2016-11-08 10:49:00 -08001309 LocalBroadcastManager.getInstance(this)
1310 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001311 if (receiver != null) {
1312 receiver.send(CONTACTS_SPLIT, new Bundle());
1313 } else {
1314 showToast(R.string.contactUnlinkedToast);
1315 }
1316 }
1317
1318 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001319 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001320 * and {@param rawContactIds2} to {@param operations}.
1321 * @return false if an error occurred, true otherwise.
1322 */
1323 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001324 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001325 if (rawContactIds1 == null || rawContactIds2 == null) {
1326 Log.e(TAG, "Invalid arguments for splitContact request");
1327 return false;
1328 }
1329 // For each pair of raw contacts, insert an aggregation exception
1330 final ContentResolver resolver = getContentResolver();
1331 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1332 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1333 for (int i = 0; i < rawContactIds1.length; i++) {
1334 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001335 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001336 // Before we get to 500 we need to flush the operations list
1337 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1338 if (!applyOperations(resolver, operations)) {
1339 return false;
1340 }
1341 operations.clear();
1342 }
1343 }
1344 }
1345 return true;
1346 }
1347
1348 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001349 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001350 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001351 */
1352 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001353 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001354 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1355 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1356 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1357 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001358
1359 // Callback intent will be invoked by the service once the contacts are joined.
1360 Intent callbackIntent = new Intent(context, callbackActivity);
1361 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001362 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1363
1364 return serviceIntent;
1365 }
1366
Brian Attwelld3946ca2015-03-03 11:13:49 -08001367 /**
1368 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1369 * No special attention is paid to where the resulting contact's name is taken from.
1370 */
Gary Mai7efa9942016-05-12 11:26:49 -07001371 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1372 ResultReceiver receiver) {
1373 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001374 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1375 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001376 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001377 return serviceIntent;
1378 }
1379
Gary Mai7efa9942016-05-12 11:26:49 -07001380 /**
1381 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1382 * No special attention is paid to where the resulting contact's name is taken from.
1383 */
1384 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1385 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1386 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001387
1388 private interface JoinContactQuery {
1389 String[] PROJECTION = {
1390 RawContacts._ID,
1391 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001392 RawContacts.DISPLAY_NAME_SOURCE,
1393 };
1394
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001395 int _ID = 0;
1396 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001397 int DISPLAY_NAME_SOURCE = 2;
1398 }
1399
1400 private interface ContactEntityQuery {
1401 String[] PROJECTION = {
1402 Contacts.Entity.DATA_ID,
1403 Contacts.Entity.CONTACT_ID,
1404 Contacts.Entity.IS_SUPER_PRIMARY,
1405 };
1406 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1407 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1408 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1409 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1410
1411 int DATA_ID = 0;
1412 int CONTACT_ID = 1;
1413 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001414 }
1415
Brian Attwelld3946ca2015-03-03 11:13:49 -08001416 private void joinSeveralContacts(Intent intent) {
1417 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001418
Gary Mai7efa9942016-05-12 11:26:49 -07001419 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001420
Brian Attwelld3946ca2015-03-03 11:13:49 -08001421 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001422 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1423 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001424 if (rawContactIds == null) {
1425 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001426 if (receiver != null) {
1427 receiver.send(BAD_ARGUMENTS, new Bundle());
1428 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001429 return;
1430 }
1431
Brian Attwelld3946ca2015-03-03 11:13:49 -08001432 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001433 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001434 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1435 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1436 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001437 for (int i = 0; i < rawContactIds.length; i++) {
1438 for (int j = 0; j < rawContactIds.length; j++) {
1439 if (i != j) {
1440 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1441 }
Walter Jang0653de32015-07-24 12:12:40 -07001442 // Before we get to 500 we need to flush the operations list
1443 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001444 if (!applyOperations(resolver, operations)) {
1445 if (receiver != null) {
1446 receiver.send(CP2_ERROR, new Bundle());
1447 }
Walter Jang0653de32015-07-24 12:12:40 -07001448 return;
1449 }
1450 operations.clear();
1451 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001452 }
1453 }
Gary Mai7efa9942016-05-12 11:26:49 -07001454 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1455 if (receiver != null) {
1456 receiver.send(CP2_ERROR, new Bundle());
1457 }
Walter Jang0653de32015-07-24 12:12:40 -07001458 return;
1459 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001460
John Shaoa3c507a2016-09-13 14:26:17 -07001461
1462 final String name = queryNameOfLinkedContacts(contactIds);
1463 if (name != null) {
1464 if (receiver != null) {
1465 final Bundle result = new Bundle();
1466 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1467 result.putString(EXTRA_DISPLAY_NAME, name);
1468 receiver.send(CONTACTS_LINKED, result);
1469 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001470 if (TextUtils.isEmpty(name)) {
1471 showToast(R.string.contactsJoinedMessage);
1472 } else {
1473 showToast(R.string.contactsJoinedNamedMessage, name);
1474 }
John Shaoa3c507a2016-09-13 14:26:17 -07001475 }
Gary Maib9065dd2016-11-08 10:49:00 -08001476 LocalBroadcastManager.getInstance(this)
1477 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001478 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001479 if (receiver != null) {
1480 receiver.send(CP2_ERROR, new Bundle());
1481 }
1482 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001483 }
Walter Jang0653de32015-07-24 12:12:40 -07001484 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001485
John Shaoa3c507a2016-09-13 14:26:17 -07001486 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001487 private String queryNameOfLinkedContacts(long[] contactIds) {
1488 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1489 final String[] whereArgs = new String[contactIds.length];
1490 for (int i = 0; i < contactIds.length; i++) {
1491 whereArgs[i] = String.valueOf(contactIds[i]);
1492 whereBuilder.append("?,");
1493 }
1494 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1495 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001496 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1497 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001498 whereBuilder.toString(), whereArgs, null);
1499
1500 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001501 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001502 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001503 try {
1504 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001505 contactId = cursor.getLong(0);
1506 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001507 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001508 }
John Shaoa3c507a2016-09-13 14:26:17 -07001509 while(cursor.moveToNext()) {
1510 if (cursor.getLong(0) != contactId) {
1511 return null;
1512 }
1513 }
James Laskeyf62b4882016-10-21 11:36:40 -07001514
1515 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1516 new ContactsPreferences(getApplicationContext()));
1517 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001518 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001519 if (cursor != null) {
1520 cursor.close();
1521 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001522 }
1523 }
1524
Walter Jang0653de32015-07-24 12:12:40 -07001525 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001526 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001527 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001528 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001529 final ContentProviderResult[] result =
1530 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1531 for (int i = 0; i < result.length; ++i) {
1532 // if no rows were modified in the operation then we count it as fail.
1533 if (result[i].count < 0) {
1534 throw new OperationApplicationException();
1535 }
1536 }
Walter Jang0653de32015-07-24 12:12:40 -07001537 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001538 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001539 FeedbackHelper.sendFeedback(this, TAG,
1540 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001541 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001542 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001543 }
1544 }
1545
Brian Attwelld3946ca2015-03-03 11:13:49 -08001546 private void joinContacts(Intent intent) {
1547 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1548 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001549
1550 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001551 // in the join UIs.
1552 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1553 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001554 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001555 return;
1556 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001557
Brian Attwell548f5c62015-01-27 17:46:46 -08001558 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001559
1560 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001561 for (int i = 0; i < rawContactIds.length; i++) {
1562 for (int j = 0; j < rawContactIds.length; j++) {
1563 if (i != j) {
1564 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1565 }
1566 }
1567 }
1568
Brian Attwelld3946ca2015-03-03 11:13:49 -08001569 final ContentResolver resolver = getContentResolver();
1570
Brian Attwell548f5c62015-01-27 17:46:46 -08001571 // Use the name for contactId1 as the name for the newly aggregated contact.
1572 final Uri contactId1Uri = ContentUris.withAppendedId(
1573 Contacts.CONTENT_URI, contactId1);
1574 final Uri entityUri = Uri.withAppendedPath(
1575 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1576 Cursor c = resolver.query(entityUri,
1577 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1578 if (c == null) {
1579 Log.e(TAG, "Unable to open Contacts DB cursor");
1580 showToast(R.string.contactSavedErrorToast);
1581 return;
1582 }
1583 long dataIdToAddSuperPrimary = -1;
1584 try {
1585 if (c.moveToFirst()) {
1586 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1587 }
1588 } finally {
1589 c.close();
1590 }
1591
1592 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1593 // display name does not change as a result of the join.
1594 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001595 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001596 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1597 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1598 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001599 operations.add(builder.build());
1600 }
1601
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001602 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001603 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001604
John Shaoa3c507a2016-09-13 14:26:17 -07001605 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001606 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001607 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001608 if (TextUtils.isEmpty(name)) {
1609 showToast(R.string.contactsJoinedMessage);
1610 } else {
1611 showToast(R.string.contactsJoinedNamedMessage, name);
1612 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001613 Uri uri = RawContacts.getContactLookupUri(resolver,
1614 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1615 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001616 LocalBroadcastManager.getInstance(this)
1617 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001618 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001619 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001620 }
1621
Gary Mai7efa9942016-05-12 11:26:49 -07001622 /**
1623 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1624 * array of the return value holds an array of raw contact ids for one contactId.
1625 * @param contactIds
1626 * @return
1627 */
1628 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1629 final long[][] rawContactIds = new long[contactIds.length][];
1630 for (int i = 0; i < contactIds.length; i++) {
1631 rawContactIds[i] = getRawContactIds(contactIds[i]);
1632 }
1633 return rawContactIds;
1634 }
1635
1636 /**
1637 * Gets the raw contact ids associated with {@param contactId}.
1638 * @param contactId
1639 * @return Array of raw contact ids.
1640 */
1641 private long[] getRawContactIds(long contactId) {
1642 final ContentResolver resolver = getContentResolver();
1643 long rawContactIds[];
1644
1645 final StringBuilder queryBuilder = new StringBuilder();
1646 queryBuilder.append(RawContacts.CONTACT_ID)
1647 .append("=")
1648 .append(String.valueOf(contactId));
1649
1650 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1651 JoinContactQuery.PROJECTION,
1652 queryBuilder.toString(),
1653 null, null);
1654 if (c == null) {
1655 Log.e(TAG, "Unable to open Contacts DB cursor");
1656 return null;
1657 }
1658 try {
1659 rawContactIds = new long[c.getCount()];
1660 for (int i = 0; i < rawContactIds.length; i++) {
1661 c.moveToPosition(i);
1662 final long rawContactId = c.getLong(JoinContactQuery._ID);
1663 rawContactIds[i] = rawContactId;
1664 }
1665 } finally {
1666 c.close();
1667 }
1668 return rawContactIds;
1669 }
1670
Brian Attwelld3946ca2015-03-03 11:13:49 -08001671 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1672 if (contactIds == null) {
1673 return null;
1674 }
1675
Brian Attwell548f5c62015-01-27 17:46:46 -08001676 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001677
1678 final StringBuilder queryBuilder = new StringBuilder();
1679 final String stringContactIds[] = new String[contactIds.length];
1680 for (int i = 0; i < contactIds.length; i++) {
1681 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1682 stringContactIds[i] = String.valueOf(contactIds[i]);
1683 if (contactIds[i] == -1) {
1684 return null;
1685 }
1686 if (i == contactIds.length -1) {
1687 break;
1688 }
1689 queryBuilder.append(" OR ");
1690 }
1691
Brian Attwell548f5c62015-01-27 17:46:46 -08001692 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1693 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001694 queryBuilder.toString(),
1695 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001696 if (c == null) {
1697 Log.e(TAG, "Unable to open Contacts DB cursor");
1698 showToast(R.string.contactSavedErrorToast);
1699 return null;
1700 }
Gary Mai7efa9942016-05-12 11:26:49 -07001701 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001702 try {
1703 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001704 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001705 return null;
1706 }
1707 rawContactIds = new long[c.getCount()];
1708 for (int i = 0; i < rawContactIds.length; i++) {
1709 c.moveToPosition(i);
1710 long rawContactId = c.getLong(JoinContactQuery._ID);
1711 rawContactIds[i] = rawContactId;
1712 }
1713 } finally {
1714 c.close();
1715 }
1716 return rawContactIds;
1717 }
1718
Brian Attwelld3946ca2015-03-03 11:13:49 -08001719 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1720 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1721 }
1722
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001723 /**
1724 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1725 */
1726 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1727 long rawContactId1, long rawContactId2) {
1728 Builder builder =
1729 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1730 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1731 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1732 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1733 operations.add(builder.build());
1734 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001735
1736 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001737 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1738 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1739 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001740 */
1741 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001742 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001743 final Builder builder =
1744 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001745 builder.withValue(AggregationExceptions.TYPE,
1746 hardSplit
1747 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1748 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001749 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1750 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1751 operations.add(builder.build());
1752 }
1753
Marcus Hagerott7333c372016-11-07 09:40:20 -08001754 /**
1755 * Returns an intent that can be used to import the contacts into targetAccount.
1756 *
1757 * @param context context to use for creating the intent
1758 * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
1759 * {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
1760 * SIM for that subscription ID will be marked as imported
1761 * @param contacts the contacts to import
1762 * @param targetAccount the account import the contacts into
1763 */
1764 public static Intent createImportFromSimIntent(Context context, int subscriptionId,
1765 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
Marcus Hagerott819214d2016-09-29 14:58:27 -07001766 return new Intent(context, ContactSaveService.class)
1767 .setAction(ACTION_IMPORT_FROM_SIM)
1768 .putExtra(EXTRA_SIM_CONTACTS, contacts)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001769 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
1770 .putExtra(EXTRA_ACCOUNT, targetAccount);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001771 }
1772
1773 private void importFromSim(Intent intent) {
1774 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1775 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
Marcus Hagerott7333c372016-11-07 09:40:20 -08001776 final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
1777 SimCard.NO_SUBSCRIPTION_ID);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001778 try {
1779 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1780 final ArrayList<SimContact> contacts =
1781 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1782 mSimContactDao.importContacts(contacts, targetAccount);
Marcus Hagerott7333c372016-11-07 09:40:20 -08001783
1784 // Update the imported state of the SIM card that was imported
1785 final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
1786 if (sim != null) {
1787 mSimContactDao.persistSimState(sim.withImportedState(true));
1788 }
1789
Marcus Hagerott819214d2016-09-29 14:58:27 -07001790 // notify success
1791 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1792 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001793 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001794 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
Marcus Hagerott819214d2016-09-29 14:58:27 -07001795 if (Log.isLoggable(TAG, Log.DEBUG)) {
1796 Log.d(TAG, "importFromSim completed successfully");
1797 }
1798 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001799 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001800 LocalBroadcastManager.getInstance(this).sendBroadcast(result
Marcus Hagerott7333c372016-11-07 09:40:20 -08001801 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
1802 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
1803 }
1804 }
1805
1806 /**
1807 * Returns an intent that can start this service and cause it to sleep for the specified time.
1808 *
1809 * This exists purely for debugging and manual testing. Since this service uses a single thread
1810 * it is useful to have a way to test behavior when work is queued up and most of the other
1811 * operations complete too quickly to simulate that under normal conditions.
1812 */
1813 public static Intent createSleepIntent(Context context, long millis) {
1814 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1815 .putExtra(EXTRA_SLEEP_DURATION, millis);
1816 }
1817
1818 private void sleepForDebugging(Intent intent) {
1819 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1820 if (Log.isLoggable(TAG, Log.DEBUG)) {
1821 Log.d(TAG, "sleeping for " + duration + "ms");
1822 }
1823 try {
1824 Thread.sleep(duration);
1825 } catch (InterruptedException e) {
1826 e.printStackTrace();
1827 }
1828 if (Log.isLoggable(TAG, Log.DEBUG)) {
1829 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001830 }
1831 }
1832
Gary Mai7efa9942016-05-12 11:26:49 -07001833 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001834 * Shows a toast on the UI thread by formatting messageId using args.
1835 * @param messageId id of message string
1836 * @param args args to format string
1837 */
1838 private void showToast(final int messageId, final Object... args) {
1839 final String message = getResources().getString(messageId, args);
1840 mMainHandler.post(new Runnable() {
1841 @Override
1842 public void run() {
1843 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1844 }
1845 });
1846 }
1847
1848
1849 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001850 * Shows a toast on the UI thread.
1851 */
1852 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001853 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001854
1855 @Override
1856 public void run() {
1857 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1858 }
1859 });
1860 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001861
1862 private void deliverCallback(final Intent callbackIntent) {
1863 mMainHandler.post(new Runnable() {
1864
1865 @Override
1866 public void run() {
1867 deliverCallbackOnUiThread(callbackIntent);
1868 }
1869 });
1870 }
1871
1872 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1873 // TODO: this assumes that if there are multiple instances of the same
1874 // activity registered, the last one registered is the one waiting for
1875 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001876 for (Listener listener : sListeners) {
1877 if (callbackIntent.getComponent().equals(
1878 ((Activity) listener).getIntent().getComponent())) {
1879 listener.onServiceCompleted(callbackIntent);
1880 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001881 }
1882 }
1883 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001884
1885 public interface GroupsDao {
1886 Uri create(String title, AccountWithDataSet account);
1887 int delete(Uri groupUri);
1888 Bundle captureDeletionUndoData(Uri groupUri);
1889 Uri undoDeletion(Bundle undoData);
1890 }
1891
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001892 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001893 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001894 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1895
1896 private static final String TAG = "GroupsDao";
1897 private final Context context;
1898 private final ContentResolver contentResolver;
1899
1900 public GroupsDaoImpl(Context context) {
1901 this(context, context.getContentResolver());
1902 }
1903
1904 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1905 this.context = context;
1906 this.contentResolver = contentResolver;
1907 }
1908
1909 public Bundle captureDeletionUndoData(Uri groupUri) {
1910 final long groupId = ContentUris.parseId(groupUri);
1911 final Bundle result = new Bundle();
1912
1913 final Cursor cursor = contentResolver.query(groupUri,
1914 new String[]{
1915 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1916 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1917 Groups.SHOULD_SYNC
1918 },
1919 Groups.DELETED + "=?", new String[] { "0" }, null);
1920 try {
1921 if (cursor.moveToFirst()) {
1922 final ContentValues groupValues = new ContentValues();
1923 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1924 result.putParcelable(KEY_GROUP_DATA, groupValues);
1925 } else {
1926 // Group doesn't exist.
1927 return result;
1928 }
1929 } finally {
1930 cursor.close();
1931 }
1932
1933 final Cursor membersCursor = contentResolver.query(
1934 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1935 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1936 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1937 final long[] memberIds = new long[membersCursor.getCount()];
1938 int i = 0;
1939 while (membersCursor.moveToNext()) {
1940 memberIds[i++] = membersCursor.getLong(0);
1941 }
1942 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1943 return result;
1944 }
1945
1946 public Uri undoDeletion(Bundle deletedGroupData) {
1947 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1948 if (groupData == null) {
1949 return null;
1950 }
1951 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1952 final long groupId = ContentUris.parseId(groupUri);
1953
1954 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1955 if (memberIds == null) {
1956 return groupUri;
1957 }
1958 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1959 for (int i = 0; i < memberIds.length; i++) {
1960 memberInsertions[i] = new ContentValues();
1961 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1962 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1963 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1964 }
1965 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1966 if (inserted != memberIds.length) {
1967 Log.e(TAG, "Could not recover some members for group deletion undo");
1968 }
1969
1970 return groupUri;
1971 }
1972
1973 public Uri create(String title, AccountWithDataSet account) {
1974 final ContentValues values = new ContentValues();
1975 values.put(Groups.TITLE, title);
1976 values.put(Groups.ACCOUNT_NAME, account.name);
1977 values.put(Groups.ACCOUNT_TYPE, account.type);
1978 values.put(Groups.DATA_SET, account.dataSet);
1979 return contentResolver.insert(Groups.CONTENT_URI, values);
1980 }
1981
1982 public int delete(Uri groupUri) {
1983 return contentResolver.delete(groupUri, null, null);
1984 }
1985 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001986
1987 /**
1988 * Keeps track of which operations have been requested but have not yet finished for this
1989 * service.
1990 */
1991 public static class State {
1992 private final CopyOnWriteArrayList<Intent> mPending;
1993
1994 public State() {
1995 mPending = new CopyOnWriteArrayList<>();
1996 }
1997
1998 public State(Collection<Intent> pendingActions) {
1999 mPending = new CopyOnWriteArrayList<>(pendingActions);
2000 }
2001
2002 public boolean isIdle() {
2003 return mPending.isEmpty();
2004 }
2005
2006 public Intent getCurrentIntent() {
2007 return mPending.isEmpty() ? null : mPending.get(0);
2008 }
2009
2010 /**
2011 * Returns the first intent requested that has the specified action or null if no intent
2012 * with that action has been requested.
2013 */
2014 public Intent getNextIntentWithAction(String action) {
2015 for (Intent intent : mPending) {
2016 if (action.equals(intent.getAction())) {
2017 return intent;
2018 }
2019 }
2020 return null;
2021 }
2022
2023 public boolean isActionPending(String action) {
2024 return getNextIntentWithAction(action) != null;
2025 }
2026
2027 private void onFinish(Intent intent) {
2028 if (mPending.isEmpty()) {
2029 return;
2030 }
2031 final String action = mPending.get(0).getAction();
2032 if (action.equals(intent.getAction())) {
2033 mPending.remove(0);
2034 }
2035 }
2036
2037 private void onStart(Intent intent) {
2038 if (intent.getAction() == null) {
2039 return;
2040 }
2041 mPending.add(intent);
2042 }
2043 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07002044}