blob: b9fc94984bb92dee3245e43fe6304a1e57a46956 [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
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Gary Mai7efa9942016-05-12 11:26:49 -070048import android.support.v4.os.ResultReceiver;
Walter Jang72f99882016-05-26 09:01:31 -070049import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
Wenyi Wangdd7d4562015-12-08 13:33:43 -080053import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080054import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070055import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080056import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080057import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070058import com.android.contacts.common.model.RawContactDelta;
59import com.android.contacts.common.model.RawContactDeltaList;
60import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080061import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070062import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080063import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070064import com.android.contacts.util.ContactPhotoUtils;
65
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070066import com.google.common.collect.Lists;
67import com.google.common.collect.Sets;
68
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080069import java.util.ArrayList;
70import java.util.HashSet;
71import java.util.List;
72import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080074/**
75 * A service responsible for saving changes to the content provider.
76 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070077public class ContactSaveService extends IntentService {
78 private static final String TAG = "ContactSaveService";
79
Katherine Kuana007e442011-07-07 09:25:34 -070080 /** Set to true in order to view logs on content provider operations */
81 private static final boolean DEBUG = false;
82
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
84
85 public static final String EXTRA_ACCOUNT_NAME = "accountName";
86 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070087 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String EXTRA_CONTENT_VALUES = "contentValues";
89 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070090 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
91 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070092
Dmitri Plotnikova0114142011-02-15 13:53:21 -080093 public static final String ACTION_SAVE_CONTACT = "saveContact";
94 public static final String EXTRA_CONTACT_STATE = "state";
95 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070096 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070097 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080098 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070099
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800100 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101 public static final String ACTION_RENAME_GROUP = "renameGroup";
102 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700103 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800104 public static final String EXTRA_GROUP_ID = "groupId";
105 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700106 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
107 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800108
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800109 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800110 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800111 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800112 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800113 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800114 public static final String EXTRA_STARRED_FLAG = "starred";
115
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800116 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
117 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
118 public static final String EXTRA_DATA_ID = "dataId";
119
Gary Mai7efa9942016-05-12 11:26:49 -0700120 public static final String ACTION_SPLIT_CONTACT = "splitContact";
121
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800122 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800123 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800124 public static final String EXTRA_CONTACT_ID1 = "contactId1";
125 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800126
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700127 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
128 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
129
130 public static final String ACTION_SET_RINGTONE = "setRingtone";
131 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
132
Gary Mai7efa9942016-05-12 11:26:49 -0700133 public static final int CP2_ERROR = 0;
134 public static final int CONTACTS_LINKED = 1;
135 public static final int CONTACTS_SPLIT = 2;
136
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700137 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
138 Data.MIMETYPE,
139 Data.IS_PRIMARY,
140 Data.DATA1,
141 Data.DATA2,
142 Data.DATA3,
143 Data.DATA4,
144 Data.DATA5,
145 Data.DATA6,
146 Data.DATA7,
147 Data.DATA8,
148 Data.DATA9,
149 Data.DATA10,
150 Data.DATA11,
151 Data.DATA12,
152 Data.DATA13,
153 Data.DATA14,
154 Data.DATA15
155 );
156
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800157 private static final int PERSIST_TRIES = 3;
158
Walter Jang0653de32015-07-24 12:12:40 -0700159 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
160
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800161 public interface Listener {
162 public void onServiceCompleted(Intent callbackIntent);
163 }
164
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100165 private static final CopyOnWriteArrayList<Listener> sListeners =
166 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800167
168 private Handler mMainHandler;
169
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700170 public ContactSaveService() {
171 super(TAG);
172 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800173 mMainHandler = new Handler(Looper.getMainLooper());
174 }
175
176 public static void registerListener(Listener listener) {
177 if (!(listener instanceof Activity)) {
178 throw new ClassCastException("Only activities can be registered to"
179 + " receive callback from " + ContactSaveService.class.getName());
180 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100181 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800182 }
183
184 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100185 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700186 }
187
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800188 /**
189 * Returns true if the ContactSaveService was started successfully and false if an exception
190 * was thrown and a Toast error message was displayed.
191 */
192 public static boolean startService(Context context, Intent intent, int saveMode) {
193 try {
194 context.startService(intent);
195 } catch (Exception exception) {
196 final int resId;
197 switch (saveMode) {
198 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
199 resId = R.string.contactUnlinkErrorToast;
200 break;
201 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
202 resId = R.string.contactJoinErrorToast;
203 break;
204 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
205 resId = R.string.contactSavedErrorToast;
206 break;
207 default:
208 resId = R.string.contactGenericErrorToast;
209 }
210 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
211 return false;
212 }
213 return true;
214 }
215
216 /**
217 * Utility method that starts service and handles exception.
218 */
219 public static void startService(Context context, Intent intent) {
220 try {
221 context.startService(intent);
222 } catch (Exception exception) {
223 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
224 }
225 }
226
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700227 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800228 public Object getSystemService(String name) {
229 Object service = super.getSystemService(name);
230 if (service != null) {
231 return service;
232 }
233
234 return getApplicationContext().getSystemService(name);
235 }
236
237 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700238 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800239 if (intent == null) {
240 Log.d(TAG, "onHandleIntent: could not handle null intent");
241 return;
242 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700243 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
244 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
245 // TODO: add more specific error string such as "Turn on Contacts
246 // permission to update your contacts"
247 showToast(R.string.contactSavedErrorToast);
248 return;
249 }
250
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700251 // Call an appropriate method. If we're sure it affects how incoming phone calls are
252 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700253 String action = intent.getAction();
254 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
255 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800256 } else if (ACTION_SAVE_CONTACT.equals(action)) {
257 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800258 } else if (ACTION_CREATE_GROUP.equals(action)) {
259 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800260 } else if (ACTION_RENAME_GROUP.equals(action)) {
261 renameGroup(intent);
262 } else if (ACTION_DELETE_GROUP.equals(action)) {
263 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700264 } else if (ACTION_UPDATE_GROUP.equals(action)) {
265 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800266 } else if (ACTION_SET_STARRED.equals(action)) {
267 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800268 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
269 setSuperPrimary(intent);
270 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
271 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800272 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
273 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800274 } else if (ACTION_DELETE_CONTACT.equals(action)) {
275 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700276 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
277 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800278 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
279 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800280 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
281 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700282 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
283 setSendToVoicemail(intent);
284 } else if (ACTION_SET_RINGTONE.equals(action)) {
285 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700286 }
287 }
288
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800289 /**
290 * Creates an intent that can be sent to this service to create a new raw contact
291 * using data presented as a set of ContentValues.
292 */
293 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700294 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700295 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800296 Intent serviceIntent = new Intent(
297 context, ContactSaveService.class);
298 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
299 if (account != null) {
300 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
301 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700302 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800303 }
304 serviceIntent.putParcelableArrayListExtra(
305 ContactSaveService.EXTRA_CONTENT_VALUES, values);
306
307 // Callback intent will be invoked by the service once the new contact is
308 // created. The service will put the URI of the new contact as "data" on
309 // the callback intent.
310 Intent callbackIntent = new Intent(context, callbackActivity);
311 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800312 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
313 return serviceIntent;
314 }
315
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700316 private void createRawContact(Intent intent) {
317 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
318 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700319 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700320 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
321 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
322
323 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
324 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
325 .withValue(RawContacts.ACCOUNT_NAME, accountName)
326 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700327 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700328 .build());
329
330 int size = valueList.size();
331 for (int i = 0; i < size; i++) {
332 ContentValues values = valueList.get(i);
333 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
334 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
335 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
336 .withValues(values)
337 .build());
338 }
339
340 ContentResolver resolver = getContentResolver();
341 ContentProviderResult[] results;
342 try {
343 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
344 } catch (Exception e) {
345 throw new RuntimeException("Failed to store new contact", e);
346 }
347
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700348 Uri rawContactUri = results[0].uri;
349 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
350
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800351 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700352 }
353
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700354 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800355 * Creates an intent that can be sent to this service to create a new raw contact
356 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800357 * This variant is more convenient to use when there is only one photo that can
358 * possibly be updated, as in the Contact Details screen.
359 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
360 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800361 */
Maurice Chu851222a2012-06-21 11:43:08 -0700362 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700363 String saveModeExtraKey, int saveMode, boolean isProfile,
364 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700365 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800366 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700367 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800368 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700369 callbackActivity, callbackAction, bundle,
370 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800371 }
372
373 /**
374 * Creates an intent that can be sent to this service to create a new raw contact
375 * using data presented as a set of ContentValues.
376 * This variant is used when multiple contacts' photos may be updated, as in the
377 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700378 *
Josh Garguse692e012012-01-18 14:53:11 -0800379 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700380 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
381 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800382 */
Maurice Chu851222a2012-06-21 11:43:08 -0700383 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700384 String saveModeExtraKey, int saveMode, boolean isProfile,
385 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700386 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000387 Intent serviceIntent = new Intent(
388 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800389 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
390 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700391 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800392 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
393
Josh Garguse692e012012-01-18 14:53:11 -0800394 if (updatedPhotos != null) {
395 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
396 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800397
Josh Garguse5d3f892012-04-11 11:56:15 -0700398 if (callbackActivity != null) {
399 // Callback intent will be invoked by the service once the contact is
400 // saved. The service will put the URI of the new contact as "data" on
401 // the callback intent.
402 Intent callbackIntent = new Intent(context, callbackActivity);
403 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700404 if (joinContactIdExtraKey != null && joinContactId != null) {
405 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
406 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700407 callbackIntent.setAction(callbackAction);
408 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
409 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800410 return serviceIntent;
411 }
412
413 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700414 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700415 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800416 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800417
Jay Shrauner08099782015-03-25 14:17:11 -0700418 if (state == null) {
419 Log.e(TAG, "Invalid arguments for saveContact request");
420 return;
421 }
422
benny.lin3a4e7a22014-01-08 10:58:08 +0800423 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800424 // Trim any empty fields, and RawContacts, before persisting
425 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700426 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800427
428 Uri lookupUri = null;
429
430 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700431
Josh Garguse692e012012-01-18 14:53:11 -0800432 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800433
Josh Gargusef15c8e2012-01-30 16:42:02 -0800434 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
435 long insertedRawContactId = -1;
436
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800437 // Attempt to persist changes
438 int tries = 0;
439 while (tries++ < PERSIST_TRIES) {
440 try {
441 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800442 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
443
444 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
445
446 for (CPOWrapper cpoWrapper : diffWrapper) {
447 diff.add(cpoWrapper.getOperation());
448 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700449
Katherine Kuana007e442011-07-07 09:25:34 -0700450 if (DEBUG) {
451 Log.v(TAG, "Content Provider Operations:");
452 for (ContentProviderOperation operation : diff) {
453 Log.v(TAG, operation.toString());
454 }
455 }
456
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700457 int numberProcessed = 0;
458 boolean batchFailed = false;
459 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
460 while (numberProcessed < diff.size()) {
461 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
462 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700463 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700464 batchFailed = true;
465 break;
466 } else {
467 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700468 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800469 }
470
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700471 if (batchFailed) {
472 // Retry save
473 continue;
474 }
475
Wenyi Wang67addcc2015-11-23 10:07:48 -0800476 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800477 if (rawContactId == -1) {
478 throw new IllegalStateException("Could not determine RawContact ID after save");
479 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800480 // We don't have to check to see if the value is still -1. If we reach here,
481 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800482 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700483 if (isProfile) {
484 // Since the profile supports local raw contacts, which may have been completely
485 // removed if all information was removed, we need to do a special query to
486 // get the lookup URI for the profile contact (if it still exists).
487 Cursor c = resolver.query(Profile.CONTENT_URI,
488 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
489 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800490 if (c == null) {
491 continue;
492 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700493 try {
Erik162b7e32011-09-20 15:23:55 -0700494 if (c.moveToFirst()) {
495 final long contactId = c.getLong(0);
496 final String lookupKey = c.getString(1);
497 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
498 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700499 } finally {
500 c.close();
501 }
502 } else {
503 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
504 rawContactId);
505 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
506 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800507 if (lookupUri != null) {
508 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
509 }
Josh Garguse692e012012-01-18 14:53:11 -0800510
511 // We can change this back to false later, if we fail to save the contact photo.
512 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800513 break;
514
515 } catch (RemoteException e) {
516 // Something went wrong, bail without success
517 Log.e(TAG, "Problem persisting user edits", e);
518 break;
519
Jay Shrauner57fca182014-01-17 14:20:50 -0800520 } catch (IllegalArgumentException e) {
521 // This is thrown by applyBatch on malformed requests
522 Log.e(TAG, "Problem persisting user edits", e);
523 showToast(R.string.contactSavedErrorToast);
524 break;
525
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800526 } catch (OperationApplicationException e) {
527 // Version consistency failed, re-parent change and try again
528 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
529 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
530 boolean first = true;
531 final int count = state.size();
532 for (int i = 0; i < count; i++) {
533 Long rawContactId = state.getRawContactId(i);
534 if (rawContactId != null && rawContactId != -1) {
535 if (!first) {
536 sb.append(',');
537 }
538 sb.append(rawContactId);
539 first = false;
540 }
541 }
542 sb.append(")");
543
544 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800545 throw new IllegalStateException(
546 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800547 }
548
Maurice Chu851222a2012-06-21 11:43:08 -0700549 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700550 isProfile
551 ? RawContactsEntity.PROFILE_CONTENT_URI
552 : RawContactsEntity.CONTENT_URI,
553 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700554 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700555
556 // Update the new state to use profile URIs if appropriate.
557 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700558 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700559 delta.setProfileQueryUri();
560 }
561 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800562 }
563 }
564
Josh Garguse692e012012-01-18 14:53:11 -0800565 // Now save any updated photos. We do this at the end to ensure that
566 // the ContactProvider already knows about newly-created contacts.
567 if (updatedPhotos != null) {
568 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700569 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800570 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800571
572 // If the raw-contact ID is negative, we are saving a new raw-contact;
573 // replace the bogus ID with the new one that we actually saved the contact at.
574 if (rawContactId < 0) {
575 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800576 }
577
Jay Shrauner511561d2015-04-02 10:35:33 -0700578 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700579 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700580 succeeded = false;
581 }
Josh Garguse692e012012-01-18 14:53:11 -0800582 }
583 }
584
Josh Garguse5d3f892012-04-11 11:56:15 -0700585 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
586 if (callbackIntent != null) {
587 if (succeeded) {
588 // Mark the intent to indicate that the save was successful (even if the lookup URI
589 // is now null). For local contacts or the local profile, it's possible that the
590 // save triggered removal of the contact, so no lookup URI would exist..
591 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
592 }
593 callbackIntent.setData(lookupUri);
594 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800595 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800596 }
597
Josh Garguse692e012012-01-18 14:53:11 -0800598 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700599 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
600 * subsets, adds the returned array to "results".
601 *
602 * @return the size of the array, if not null; -1 when the array is null.
603 */
604 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
605 ContentProviderResult[] results, ContentResolver resolver)
606 throws RemoteException, OperationApplicationException {
607 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
608 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
609 subset.addAll(diff.subList(offset, offset + subsetCount));
610 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
611 .AUTHORITY, subset);
612 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
613 return -1;
614 }
615 for (ContentProviderResult c : subsetResult) {
616 results[offset++] = c;
617 }
618 return subsetResult.length;
619 }
620
621 /**
Josh Garguse692e012012-01-18 14:53:11 -0800622 * Save updated photo for the specified raw-contact.
623 * @return true for success, false for failure
624 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800625 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800626 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800627 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
628 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
629
benny.lin3a4e7a22014-01-08 10:58:08 +0800630 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800631 }
632
Josh Gargusef15c8e2012-01-30 16:42:02 -0800633 /**
634 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
635 */
Maurice Chu851222a2012-06-21 11:43:08 -0700636 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800637 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800638 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800639 long existingRawContactId = state.findRawContactId();
640 if (existingRawContactId != -1) {
641 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800642 }
643
Wenyi Wang67addcc2015-11-23 10:07:48 -0800644 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800645 }
646
647 /**
648 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
649 */
650 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800651 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800652 if (results == null) {
653 return -1;
654 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800655 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800656 final int numResults = results.length;
657 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800658 final CPOWrapper cpoWrapper = diffWrapper.get(i);
659 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
660 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
661 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800662 return ContentUris.parseId(results[i].uri);
663 }
664 }
665 return -1;
666 }
667
668 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700669 * Creates an intent that can be sent to this service to create a new group as
670 * well as add new members at the same time.
671 *
672 * @param context of the application
673 * @param account in which the group should be created
674 * @param label is the name of the group (cannot be null)
675 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
676 * should be added to the group
677 * @param callbackActivity is the activity to send the callback intent to
678 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700679 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700680 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700681 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700682 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800683 Intent serviceIntent = new Intent(context, ContactSaveService.class);
684 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
685 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700687 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700689 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700690
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800691 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700692 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800693 Intent callbackIntent = new Intent(context, callbackActivity);
694 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700695 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800696
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700697 return serviceIntent;
698 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800699
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800700 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700701 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
702 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
703 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
704 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700705 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800706
707 ContentValues values = new ContentValues();
708 values.put(Groups.ACCOUNT_TYPE, accountType);
709 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700710 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800711 values.put(Groups.TITLE, label);
712
Katherine Kuan717e3432011-07-13 17:03:24 -0700713 final ContentResolver resolver = getContentResolver();
714
715 // Create the new group
716 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
717
718 // If there's no URI, then the insertion failed. Abort early because group members can't be
719 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800720 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700721 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800722 return;
723 }
724
Katherine Kuan717e3432011-07-13 17:03:24 -0700725 // Add new group members
726 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
727
728 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
729 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800730 values.clear();
731 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
732 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
733
734 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700735 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700736 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800737 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800738 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800739 }
740
741 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800742 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800743 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700744 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700745 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800746 Intent serviceIntent = new Intent(context, ContactSaveService.class);
747 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
748 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
749 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700750
751 // Callback intent will be invoked by the service once the group is renamed.
752 Intent callbackIntent = new Intent(context, callbackActivity);
753 callbackIntent.setAction(callbackAction);
754 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
755
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800756 return serviceIntent;
757 }
758
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800759 private void renameGroup(Intent intent) {
760 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
761 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
762
763 if (groupId == -1) {
764 Log.e(TAG, "Invalid arguments for renameGroup request");
765 return;
766 }
767
768 ContentValues values = new ContentValues();
769 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700770 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
771 getContentResolver().update(groupUri, values, null, null);
772
773 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
774 callbackIntent.setData(groupUri);
775 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800776 }
777
778 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800779 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800780 */
Walter Jang72f99882016-05-26 09:01:31 -0700781 public static Intent createGroupDeletionIntent(Context context, long groupId,
782 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800783 Intent serviceIntent = new Intent(context, ContactSaveService.class);
784 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800785 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700786
787 // Callback intent will be invoked by the service once the group is updated
788 if (callbackActivity != null && !TextUtils.isEmpty(callbackAction)) {
789 final Intent callbackIntent = new Intent(context, callbackActivity);
790 callbackIntent.setAction(callbackAction);
791 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
792 }
793
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800794 return serviceIntent;
795 }
796
797 private void deleteGroup(Intent intent) {
798 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
799 if (groupId == -1) {
800 Log.e(TAG, "Invalid arguments for deleteGroup request");
801 return;
802 }
803
804 getContentResolver().delete(
805 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
Walter Jang72f99882016-05-26 09:01:31 -0700806
807 final Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
808 if (callbackIntent != null) {
809 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
810 callbackIntent.setData(groupUri);
811 deliverCallback(callbackIntent);
812 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800813 }
814
815 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700816 * Creates an intent that can be sent to this service to rename a group as
817 * well as add and remove members from the group.
818 *
819 * @param context of the application
820 * @param groupId of the group that should be modified
821 * @param newLabel is the updated name of the group (can be null if the name
822 * should not be updated)
823 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
824 * should be added to the group
825 * @param rawContactsToRemove is an array of raw contact IDs for contacts
826 * that should be removed from the group
827 * @param callbackActivity is the activity to send the callback intent to
828 * @param callbackAction is the intent action for the callback intent
829 */
830 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
831 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700832 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700833 Intent serviceIntent = new Intent(context, ContactSaveService.class);
834 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
835 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
836 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
837 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
838 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
839 rawContactsToRemove);
840
841 // Callback intent will be invoked by the service once the group is updated
842 Intent callbackIntent = new Intent(context, callbackActivity);
843 callbackIntent.setAction(callbackAction);
844 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
845
846 return serviceIntent;
847 }
848
849 private void updateGroup(Intent intent) {
850 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
851 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
852 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
853 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
854
855 if (groupId == -1) {
856 Log.e(TAG, "Invalid arguments for updateGroup request");
857 return;
858 }
859
860 final ContentResolver resolver = getContentResolver();
861 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
862
863 // Update group name if necessary
864 if (label != null) {
865 ContentValues values = new ContentValues();
866 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700867 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700868 }
869
Katherine Kuan717e3432011-07-13 17:03:24 -0700870 // Add and remove members if necessary
871 addMembersToGroup(resolver, rawContactsToAdd, groupId);
872 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
873
874 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
875 callbackIntent.setData(groupUri);
876 deliverCallback(callbackIntent);
877 }
878
Daniel Lehmann18958a22012-02-28 17:45:25 -0800879 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700880 long groupId) {
881 if (rawContactsToAdd == null) {
882 return;
883 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700884 for (long rawContactId : rawContactsToAdd) {
885 try {
886 final ArrayList<ContentProviderOperation> rawContactOperations =
887 new ArrayList<ContentProviderOperation>();
888
889 // Build an assert operation to ensure the contact is not already in the group
890 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
891 .newAssertQuery(Data.CONTENT_URI);
892 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
893 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
894 new String[] { String.valueOf(rawContactId),
895 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
896 assertBuilder.withExpectedCount(0);
897 rawContactOperations.add(assertBuilder.build());
898
899 // Build an insert operation to add the contact to the group
900 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
901 .newInsert(Data.CONTENT_URI);
902 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
903 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
904 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
905 rawContactOperations.add(insertBuilder.build());
906
907 if (DEBUG) {
908 for (ContentProviderOperation operation : rawContactOperations) {
909 Log.v(TAG, operation.toString());
910 }
911 }
912
913 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700914 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800915 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700916 }
917 } catch (RemoteException e) {
918 // Something went wrong, bail without success
919 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
920 String.valueOf(rawContactId), e);
921 } catch (OperationApplicationException e) {
922 // The assert could have failed because the contact is already in the group,
923 // just continue to the next contact
924 Log.w(TAG, "Assert failed in adding raw contact ID " +
925 String.valueOf(rawContactId) + ". Already exists in group " +
926 String.valueOf(groupId), e);
927 }
928 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700929 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700930
Daniel Lehmann18958a22012-02-28 17:45:25 -0800931 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700932 long groupId) {
933 if (rawContactsToRemove == null) {
934 return;
935 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700936 for (long rawContactId : rawContactsToRemove) {
937 // Apply the delete operation on the data row for the given raw contact's
938 // membership in the given group. If no contact matches the provided selection, then
939 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800940 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700941 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
942 new String[] { String.valueOf(rawContactId),
943 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
944 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700945 }
946
947 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800948 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800949 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800950 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
951 Intent serviceIntent = new Intent(context, ContactSaveService.class);
952 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
953 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
954 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
955
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800956 return serviceIntent;
957 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800958
959 private void setStarred(Intent intent) {
960 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
961 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
962 if (contactUri == null) {
963 Log.e(TAG, "Invalid arguments for setStarred request");
964 return;
965 }
966
967 final ContentValues values = new ContentValues(1);
968 values.put(Contacts.STARRED, value);
969 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700970
971 // Undemote the contact if necessary
972 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
973 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800974 if (c == null) {
975 return;
976 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700977 try {
978 if (c.moveToFirst()) {
979 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700980
981 // Don't bother undemoting if this contact is the user's profile.
982 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800983 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700984 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700985 }
986 } finally {
987 c.close();
988 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800989 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800990
991 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700992 * Creates an intent that can be sent to this service to set the redirect to voicemail.
993 */
994 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
995 boolean value) {
996 Intent serviceIntent = new Intent(context, ContactSaveService.class);
997 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
998 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
999 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1000
1001 return serviceIntent;
1002 }
1003
1004 private void setSendToVoicemail(Intent intent) {
1005 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1006 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1007 if (contactUri == null) {
1008 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1009 return;
1010 }
1011
1012 final ContentValues values = new ContentValues(1);
1013 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1014 getContentResolver().update(contactUri, values, null, null);
1015 }
1016
1017 /**
1018 * Creates an intent that can be sent to this service to save the contact's ringtone.
1019 */
1020 public static Intent createSetRingtone(Context context, Uri contactUri,
1021 String value) {
1022 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1023 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1024 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1025 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1026
1027 return serviceIntent;
1028 }
1029
1030 private void setRingtone(Intent intent) {
1031 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1032 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1033 if (contactUri == null) {
1034 Log.e(TAG, "Invalid arguments for setRingtone");
1035 return;
1036 }
1037 ContentValues values = new ContentValues(1);
1038 values.put(Contacts.CUSTOM_RINGTONE, value);
1039 getContentResolver().update(contactUri, values, null, null);
1040 }
1041
1042 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001043 * Creates an intent that sets the selected data item as super primary (default)
1044 */
1045 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1046 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1047 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1048 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1049 return serviceIntent;
1050 }
1051
1052 private void setSuperPrimary(Intent intent) {
1053 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1054 if (dataId == -1) {
1055 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1056 return;
1057 }
1058
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001059 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001060 }
1061
1062 /**
1063 * Creates an intent that clears the primary flag of all data items that belong to the same
1064 * raw_contact as the given data item. Will only clear, if the data item was primary before
1065 * this call
1066 */
1067 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1068 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1069 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1070 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1071 return serviceIntent;
1072 }
1073
1074 private void clearPrimary(Intent intent) {
1075 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1076 if (dataId == -1) {
1077 Log.e(TAG, "Invalid arguments for clearPrimary request");
1078 return;
1079 }
1080
1081 // Update the primary values in the data record.
1082 ContentValues values = new ContentValues(1);
1083 values.put(Data.IS_SUPER_PRIMARY, 0);
1084 values.put(Data.IS_PRIMARY, 0);
1085
1086 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1087 values, null, null);
1088 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001089
1090 /**
1091 * Creates an intent that can be sent to this service to delete a contact.
1092 */
1093 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1094 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1095 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1096 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1097 return serviceIntent;
1098 }
1099
Brian Attwelld2962a32015-03-02 14:48:50 -08001100 /**
1101 * Creates an intent that can be sent to this service to delete multiple contacts.
1102 */
1103 public static Intent createDeleteMultipleContactsIntent(Context context,
1104 long[] contactIds) {
1105 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1106 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1107 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1108 return serviceIntent;
1109 }
1110
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001111 private void deleteContact(Intent intent) {
1112 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1113 if (contactUri == null) {
1114 Log.e(TAG, "Invalid arguments for deleteContact request");
1115 return;
1116 }
1117
1118 getContentResolver().delete(contactUri, null, null);
1119 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001120
Brian Attwelld2962a32015-03-02 14:48:50 -08001121 private void deleteMultipleContacts(Intent intent) {
1122 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1123 if (contactIds == null) {
1124 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1125 return;
1126 }
1127 for (long contactId : contactIds) {
1128 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1129 getContentResolver().delete(contactUri, null, null);
1130 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001131 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1132 .contacts_deleted_toast, contactIds.length);
1133 mMainHandler.post(new Runnable() {
1134 @Override
1135 public void run() {
1136 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1137 .show();
1138 }
1139 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001140 }
1141
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001142 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001143 * Creates an intent that can be sent to this service to split a contact into it's constituent
1144 * pieces.
1145 */
1146 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1147 ResultReceiver receiver) {
1148 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1149 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1150 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1151 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1152 return serviceIntent;
1153 }
1154
1155 private void splitContact(Intent intent) {
1156 final long rawContactIds[][] = (long[][]) intent
1157 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
1158 if (rawContactIds == null) {
1159 Log.e(TAG, "Invalid argument for splitContact request");
1160 return;
1161 }
1162 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1163 final ContentResolver resolver = getContentResolver();
1164 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
1165 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
1166 for (int i = 0; i < rawContactIds.length; i++) {
1167 for (int j = 0; j < rawContactIds.length; j++) {
1168 if (i != j) {
1169 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1170 if (receiver != null) {
1171 receiver.send(CP2_ERROR, new Bundle());
1172 return;
1173 }
1174 }
1175 }
1176 }
1177 }
1178 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1179 if (receiver != null) {
1180 receiver.send(CP2_ERROR, new Bundle());
1181 }
1182 return;
1183 }
1184 if (receiver != null) {
1185 receiver.send(CONTACTS_SPLIT, new Bundle());
1186 } else {
1187 showToast(R.string.contactUnlinkedToast);
1188 }
1189 }
1190
1191 /**
1192 * Adds insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
1193 * and {@param rawContactIds2} to {@param operations}.
1194 * @return false if an error occurred, true otherwise.
1195 */
1196 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1197 long[] rawContactIds1, long[] rawContactIds2) {
1198 if (rawContactIds1 == null || rawContactIds2 == null) {
1199 Log.e(TAG, "Invalid arguments for splitContact request");
1200 return false;
1201 }
1202 // For each pair of raw contacts, insert an aggregation exception
1203 final ContentResolver resolver = getContentResolver();
1204 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1205 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1206 for (int i = 0; i < rawContactIds1.length; i++) {
1207 for (int j = 0; j < rawContactIds2.length; j++) {
1208 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1209 // Before we get to 500 we need to flush the operations list
1210 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1211 if (!applyOperations(resolver, operations)) {
1212 return false;
1213 }
1214 operations.clear();
1215 }
1216 }
1217 }
1218 return true;
1219 }
1220
1221 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001222 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001223 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001224 */
1225 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001226 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001227 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1228 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1229 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1230 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001231
1232 // Callback intent will be invoked by the service once the contacts are joined.
1233 Intent callbackIntent = new Intent(context, callbackActivity);
1234 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001235 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1236
1237 return serviceIntent;
1238 }
1239
Brian Attwelld3946ca2015-03-03 11:13:49 -08001240 /**
1241 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1242 * No special attention is paid to where the resulting contact's name is taken from.
1243 */
Gary Mai7efa9942016-05-12 11:26:49 -07001244 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1245 ResultReceiver receiver) {
1246 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001247 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1248 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001249 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001250 return serviceIntent;
1251 }
1252
Gary Mai7efa9942016-05-12 11:26:49 -07001253 /**
1254 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1255 * No special attention is paid to where the resulting contact's name is taken from.
1256 */
1257 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1258 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1259 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001260
1261 private interface JoinContactQuery {
1262 String[] PROJECTION = {
1263 RawContacts._ID,
1264 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001265 RawContacts.DISPLAY_NAME_SOURCE,
1266 };
1267
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001268 int _ID = 0;
1269 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001270 int DISPLAY_NAME_SOURCE = 2;
1271 }
1272
1273 private interface ContactEntityQuery {
1274 String[] PROJECTION = {
1275 Contacts.Entity.DATA_ID,
1276 Contacts.Entity.CONTACT_ID,
1277 Contacts.Entity.IS_SUPER_PRIMARY,
1278 };
1279 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1280 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1281 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1282 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1283
1284 int DATA_ID = 0;
1285 int CONTACT_ID = 1;
1286 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001287 }
1288
Brian Attwelld3946ca2015-03-03 11:13:49 -08001289 private void joinSeveralContacts(Intent intent) {
1290 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Gary Mai7efa9942016-05-12 11:26:49 -07001291 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001292
Brian Attwelld3946ca2015-03-03 11:13:49 -08001293 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001294 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1295 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001296 if (rawContactIds == null) {
1297 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001298 return;
1299 }
1300
Brian Attwelld3946ca2015-03-03 11:13:49 -08001301 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001302 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001303 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1304 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1305 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001306 for (int i = 0; i < rawContactIds.length; i++) {
1307 for (int j = 0; j < rawContactIds.length; j++) {
1308 if (i != j) {
1309 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1310 }
Walter Jang0653de32015-07-24 12:12:40 -07001311 // Before we get to 500 we need to flush the operations list
1312 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001313 if (!applyOperations(resolver, operations)) {
1314 if (receiver != null) {
1315 receiver.send(CP2_ERROR, new Bundle());
1316 }
Walter Jang0653de32015-07-24 12:12:40 -07001317 return;
1318 }
1319 operations.clear();
1320 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001321 }
1322 }
Gary Mai7efa9942016-05-12 11:26:49 -07001323 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1324 if (receiver != null) {
1325 receiver.send(CP2_ERROR, new Bundle());
1326 }
Walter Jang0653de32015-07-24 12:12:40 -07001327 return;
1328 }
Gary Mai7efa9942016-05-12 11:26:49 -07001329 if (receiver != null) {
1330 final Bundle result = new Bundle();
1331 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1332 receiver.send(CONTACTS_LINKED, result);
1333 } else {
1334 showToast(R.string.contactsJoinedMessage);
1335 }
Walter Jang0653de32015-07-24 12:12:40 -07001336 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001337
Walter Jang0653de32015-07-24 12:12:40 -07001338 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001339 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001340 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001341 try {
1342 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001343 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001344 } catch (RemoteException | OperationApplicationException e) {
1345 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1346 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001347 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001348 }
1349 }
1350
Brian Attwelld3946ca2015-03-03 11:13:49 -08001351 private void joinContacts(Intent intent) {
1352 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1353 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001354
1355 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001356 // in the join UIs.
1357 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1358 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001359 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001360 return;
1361 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001362
Brian Attwell548f5c62015-01-27 17:46:46 -08001363 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001364
1365 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001366 for (int i = 0; i < rawContactIds.length; i++) {
1367 for (int j = 0; j < rawContactIds.length; j++) {
1368 if (i != j) {
1369 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1370 }
1371 }
1372 }
1373
Brian Attwelld3946ca2015-03-03 11:13:49 -08001374 final ContentResolver resolver = getContentResolver();
1375
Brian Attwell548f5c62015-01-27 17:46:46 -08001376 // Use the name for contactId1 as the name for the newly aggregated contact.
1377 final Uri contactId1Uri = ContentUris.withAppendedId(
1378 Contacts.CONTENT_URI, contactId1);
1379 final Uri entityUri = Uri.withAppendedPath(
1380 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1381 Cursor c = resolver.query(entityUri,
1382 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1383 if (c == null) {
1384 Log.e(TAG, "Unable to open Contacts DB cursor");
1385 showToast(R.string.contactSavedErrorToast);
1386 return;
1387 }
1388 long dataIdToAddSuperPrimary = -1;
1389 try {
1390 if (c.moveToFirst()) {
1391 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1392 }
1393 } finally {
1394 c.close();
1395 }
1396
1397 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1398 // display name does not change as a result of the join.
1399 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001400 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001401 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1402 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1403 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001404 operations.add(builder.build());
1405 }
1406
1407 boolean success = false;
1408 // Apply all aggregation exceptions as one batch
1409 try {
1410 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001411 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001412 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001413 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001414 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001415 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001416 }
1417
1418 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1419 if (success) {
1420 Uri uri = RawContacts.getContactLookupUri(resolver,
1421 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1422 callbackIntent.setData(uri);
1423 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001424 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001425 }
1426
Gary Mai7efa9942016-05-12 11:26:49 -07001427 /**
1428 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1429 * array of the return value holds an array of raw contact ids for one contactId.
1430 * @param contactIds
1431 * @return
1432 */
1433 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1434 final long[][] rawContactIds = new long[contactIds.length][];
1435 for (int i = 0; i < contactIds.length; i++) {
1436 rawContactIds[i] = getRawContactIds(contactIds[i]);
1437 }
1438 return rawContactIds;
1439 }
1440
1441 /**
1442 * Gets the raw contact ids associated with {@param contactId}.
1443 * @param contactId
1444 * @return Array of raw contact ids.
1445 */
1446 private long[] getRawContactIds(long contactId) {
1447 final ContentResolver resolver = getContentResolver();
1448 long rawContactIds[];
1449
1450 final StringBuilder queryBuilder = new StringBuilder();
1451 queryBuilder.append(RawContacts.CONTACT_ID)
1452 .append("=")
1453 .append(String.valueOf(contactId));
1454
1455 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1456 JoinContactQuery.PROJECTION,
1457 queryBuilder.toString(),
1458 null, null);
1459 if (c == null) {
1460 Log.e(TAG, "Unable to open Contacts DB cursor");
1461 return null;
1462 }
1463 try {
1464 rawContactIds = new long[c.getCount()];
1465 for (int i = 0; i < rawContactIds.length; i++) {
1466 c.moveToPosition(i);
1467 final long rawContactId = c.getLong(JoinContactQuery._ID);
1468 rawContactIds[i] = rawContactId;
1469 }
1470 } finally {
1471 c.close();
1472 }
1473 return rawContactIds;
1474 }
1475
Brian Attwelld3946ca2015-03-03 11:13:49 -08001476 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1477 if (contactIds == null) {
1478 return null;
1479 }
1480
Brian Attwell548f5c62015-01-27 17:46:46 -08001481 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001482
1483 final StringBuilder queryBuilder = new StringBuilder();
1484 final String stringContactIds[] = new String[contactIds.length];
1485 for (int i = 0; i < contactIds.length; i++) {
1486 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1487 stringContactIds[i] = String.valueOf(contactIds[i]);
1488 if (contactIds[i] == -1) {
1489 return null;
1490 }
1491 if (i == contactIds.length -1) {
1492 break;
1493 }
1494 queryBuilder.append(" OR ");
1495 }
1496
Brian Attwell548f5c62015-01-27 17:46:46 -08001497 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1498 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001499 queryBuilder.toString(),
1500 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001501 if (c == null) {
1502 Log.e(TAG, "Unable to open Contacts DB cursor");
1503 showToast(R.string.contactSavedErrorToast);
1504 return null;
1505 }
Gary Mai7efa9942016-05-12 11:26:49 -07001506 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001507 try {
1508 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001509 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001510 return null;
1511 }
1512 rawContactIds = new long[c.getCount()];
1513 for (int i = 0; i < rawContactIds.length; i++) {
1514 c.moveToPosition(i);
1515 long rawContactId = c.getLong(JoinContactQuery._ID);
1516 rawContactIds[i] = rawContactId;
1517 }
1518 } finally {
1519 c.close();
1520 }
1521 return rawContactIds;
1522 }
1523
Brian Attwelld3946ca2015-03-03 11:13:49 -08001524 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1525 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1526 }
1527
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001528 /**
1529 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1530 */
1531 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1532 long rawContactId1, long rawContactId2) {
1533 Builder builder =
1534 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1535 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1536 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1537 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1538 operations.add(builder.build());
1539 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001540
1541 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001542 * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation.
1543 */
1544 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1545 long rawContactId1, long rawContactId2) {
1546 final Builder builder =
1547 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1548 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
1549 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1550 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1551 operations.add(builder.build());
1552 }
1553
1554 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001555 * Shows a toast on the UI thread.
1556 */
1557 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001558 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001559
1560 @Override
1561 public void run() {
1562 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1563 }
1564 });
1565 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001566
1567 private void deliverCallback(final Intent callbackIntent) {
1568 mMainHandler.post(new Runnable() {
1569
1570 @Override
1571 public void run() {
1572 deliverCallbackOnUiThread(callbackIntent);
1573 }
1574 });
1575 }
1576
1577 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1578 // TODO: this assumes that if there are multiple instances of the same
1579 // activity registered, the last one registered is the one waiting for
1580 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001581 for (Listener listener : sListeners) {
1582 if (callbackIntent.getComponent().equals(
1583 ((Activity) listener).getIntent().getComponent())) {
1584 listener.onServiceCompleted(callbackIntent);
1585 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001586 }
1587 }
1588 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001589}