blob: 5c9c899c37410059ca6a00e78c6b38990c8d9984 [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;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Wenyi Wangdd7d4562015-12-08 13:33:43 -080051import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080052import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070053import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080054import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080055import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070056import com.android.contacts.common.model.RawContactDelta;
57import com.android.contacts.common.model.RawContactDeltaList;
58import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080059import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070060import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080061import com.android.contacts.compat.PinnedPositionsCompat;
Walter Jang0ae4a932016-02-19 15:01:32 -080062import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor.SaveMode;
Yorke Lee637a38e2013-09-14 08:36:33 -070063import com.android.contacts.util.ContactPhotoUtils;
64
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070065import com.google.common.collect.Lists;
66import com.google.common.collect.Sets;
67
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080068import java.util.ArrayList;
69import java.util.HashSet;
70import java.util.List;
71import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070072
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080073/**
74 * A service responsible for saving changes to the content provider.
75 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070076public class ContactSaveService extends IntentService {
77 private static final String TAG = "ContactSaveService";
78
Katherine Kuana007e442011-07-07 09:25:34 -070079 /** Set to true in order to view logs on content provider operations */
80 private static final boolean DEBUG = false;
81
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070082 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
83
84 public static final String EXTRA_ACCOUNT_NAME = "accountName";
85 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070086 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070087 public static final String EXTRA_CONTENT_VALUES = "contentValues";
88 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
89
Dmitri Plotnikova0114142011-02-15 13:53:21 -080090 public static final String ACTION_SAVE_CONTACT = "saveContact";
91 public static final String EXTRA_CONTACT_STATE = "state";
92 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070093 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070094 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080095 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070096
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080097 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080098 public static final String ACTION_RENAME_GROUP = "renameGroup";
99 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700100 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101 public static final String EXTRA_GROUP_ID = "groupId";
102 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700103 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
104 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800105
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800107 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800108 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800109 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800110 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800111 public static final String EXTRA_STARRED_FLAG = "starred";
112
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800113 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
114 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
115 public static final String EXTRA_DATA_ID = "dataId";
116
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800117 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800118 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800119 public static final String EXTRA_CONTACT_ID1 = "contactId1";
120 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800121
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700122 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
123 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
124
125 public static final String ACTION_SET_RINGTONE = "setRingtone";
126 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
127
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700128 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
129 Data.MIMETYPE,
130 Data.IS_PRIMARY,
131 Data.DATA1,
132 Data.DATA2,
133 Data.DATA3,
134 Data.DATA4,
135 Data.DATA5,
136 Data.DATA6,
137 Data.DATA7,
138 Data.DATA8,
139 Data.DATA9,
140 Data.DATA10,
141 Data.DATA11,
142 Data.DATA12,
143 Data.DATA13,
144 Data.DATA14,
145 Data.DATA15
146 );
147
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800148 private static final int PERSIST_TRIES = 3;
149
Walter Jang0653de32015-07-24 12:12:40 -0700150 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
151
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800152 public interface Listener {
153 public void onServiceCompleted(Intent callbackIntent);
154 }
155
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100156 private static final CopyOnWriteArrayList<Listener> sListeners =
157 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800158
159 private Handler mMainHandler;
160
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700161 public ContactSaveService() {
162 super(TAG);
163 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800164 mMainHandler = new Handler(Looper.getMainLooper());
165 }
166
167 public static void registerListener(Listener listener) {
168 if (!(listener instanceof Activity)) {
169 throw new ClassCastException("Only activities can be registered to"
170 + " receive callback from " + ContactSaveService.class.getName());
171 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100172 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800173 }
174
175 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100176 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700177 }
178
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800179 /**
180 * Returns true if the ContactSaveService was started successfully and false if an exception
181 * was thrown and a Toast error message was displayed.
182 */
183 public static boolean startService(Context context, Intent intent, int saveMode) {
184 try {
185 context.startService(intent);
186 } catch (Exception exception) {
187 final int resId;
188 switch (saveMode) {
189 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
190 resId = R.string.contactUnlinkErrorToast;
191 break;
192 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
193 resId = R.string.contactJoinErrorToast;
194 break;
195 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
196 resId = R.string.contactSavedErrorToast;
197 break;
198 default:
199 resId = R.string.contactGenericErrorToast;
200 }
201 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
202 return false;
203 }
204 return true;
205 }
206
207 /**
208 * Utility method that starts service and handles exception.
209 */
210 public static void startService(Context context, Intent intent) {
211 try {
212 context.startService(intent);
213 } catch (Exception exception) {
214 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
215 }
216 }
217
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700218 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800219 public Object getSystemService(String name) {
220 Object service = super.getSystemService(name);
221 if (service != null) {
222 return service;
223 }
224
225 return getApplicationContext().getSystemService(name);
226 }
227
228 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700229 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800230 if (intent == null) {
231 Log.d(TAG, "onHandleIntent: could not handle null intent");
232 return;
233 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700234 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
235 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
236 // TODO: add more specific error string such as "Turn on Contacts
237 // permission to update your contacts"
238 showToast(R.string.contactSavedErrorToast);
239 return;
240 }
241
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700242 // Call an appropriate method. If we're sure it affects how incoming phone calls are
243 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700244 String action = intent.getAction();
245 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
246 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800247 } else if (ACTION_SAVE_CONTACT.equals(action)) {
248 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800249 } else if (ACTION_CREATE_GROUP.equals(action)) {
250 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800251 } else if (ACTION_RENAME_GROUP.equals(action)) {
252 renameGroup(intent);
253 } else if (ACTION_DELETE_GROUP.equals(action)) {
254 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700255 } else if (ACTION_UPDATE_GROUP.equals(action)) {
256 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800257 } else if (ACTION_SET_STARRED.equals(action)) {
258 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800259 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
260 setSuperPrimary(intent);
261 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
262 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800263 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
264 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800265 } else if (ACTION_DELETE_CONTACT.equals(action)) {
266 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800267 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
268 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800269 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
270 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700271 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
272 setSendToVoicemail(intent);
273 } else if (ACTION_SET_RINGTONE.equals(action)) {
274 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700275 }
276 }
277
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800278 /**
279 * Creates an intent that can be sent to this service to create a new raw contact
280 * using data presented as a set of ContentValues.
281 */
282 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700283 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700284 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800285 Intent serviceIntent = new Intent(
286 context, ContactSaveService.class);
287 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
288 if (account != null) {
289 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
290 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700291 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800292 }
293 serviceIntent.putParcelableArrayListExtra(
294 ContactSaveService.EXTRA_CONTENT_VALUES, values);
295
296 // Callback intent will be invoked by the service once the new contact is
297 // created. The service will put the URI of the new contact as "data" on
298 // the callback intent.
299 Intent callbackIntent = new Intent(context, callbackActivity);
300 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800301 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
302 return serviceIntent;
303 }
304
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700305 private void createRawContact(Intent intent) {
306 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
307 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700308 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700309 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
310 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
311
312 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
313 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
314 .withValue(RawContacts.ACCOUNT_NAME, accountName)
315 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700316 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700317 .build());
318
319 int size = valueList.size();
320 for (int i = 0; i < size; i++) {
321 ContentValues values = valueList.get(i);
322 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
323 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
324 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
325 .withValues(values)
326 .build());
327 }
328
329 ContentResolver resolver = getContentResolver();
330 ContentProviderResult[] results;
331 try {
332 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
333 } catch (Exception e) {
334 throw new RuntimeException("Failed to store new contact", e);
335 }
336
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700337 Uri rawContactUri = results[0].uri;
338 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
339
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800340 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700341 }
342
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700343 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800344 * Creates an intent that can be sent to this service to create a new raw contact
345 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800346 * This variant is more convenient to use when there is only one photo that can
347 * possibly be updated, as in the Contact Details screen.
348 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
349 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800350 */
Maurice Chu851222a2012-06-21 11:43:08 -0700351 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700352 String saveModeExtraKey, int saveMode, boolean isProfile,
353 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700354 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800355 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700356 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800357 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700358 callbackActivity, callbackAction, bundle,
359 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800360 }
361
362 /**
363 * Creates an intent that can be sent to this service to create a new raw contact
364 * using data presented as a set of ContentValues.
365 * This variant is used when multiple contacts' photos may be updated, as in the
366 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700367 *
Josh Garguse692e012012-01-18 14:53:11 -0800368 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700369 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
370 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800371 */
Maurice Chu851222a2012-06-21 11:43:08 -0700372 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700373 String saveModeExtraKey, int saveMode, boolean isProfile,
374 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700375 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000376 Intent serviceIntent = new Intent(
377 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800378 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
379 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700380 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800381 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
382
Josh Garguse692e012012-01-18 14:53:11 -0800383 if (updatedPhotos != null) {
384 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
385 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800386
Josh Garguse5d3f892012-04-11 11:56:15 -0700387 if (callbackActivity != null) {
388 // Callback intent will be invoked by the service once the contact is
389 // saved. The service will put the URI of the new contact as "data" on
390 // the callback intent.
391 Intent callbackIntent = new Intent(context, callbackActivity);
392 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700393 if (joinContactIdExtraKey != null && joinContactId != null) {
394 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
395 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700396 callbackIntent.setAction(callbackAction);
397 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
398 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800399 return serviceIntent;
400 }
401
402 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700403 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700404 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800405 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800406
Jay Shrauner08099782015-03-25 14:17:11 -0700407 if (state == null) {
408 Log.e(TAG, "Invalid arguments for saveContact request");
409 return;
410 }
411
benny.lin3a4e7a22014-01-08 10:58:08 +0800412 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800413 // Trim any empty fields, and RawContacts, before persisting
414 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700415 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800416
417 Uri lookupUri = null;
418
419 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700420
Josh Garguse692e012012-01-18 14:53:11 -0800421 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800422
Josh Gargusef15c8e2012-01-30 16:42:02 -0800423 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
424 long insertedRawContactId = -1;
425
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800426 // Attempt to persist changes
427 int tries = 0;
428 while (tries++ < PERSIST_TRIES) {
429 try {
430 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800431 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
432
433 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
434
435 for (CPOWrapper cpoWrapper : diffWrapper) {
436 diff.add(cpoWrapper.getOperation());
437 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700438
Katherine Kuana007e442011-07-07 09:25:34 -0700439 if (DEBUG) {
440 Log.v(TAG, "Content Provider Operations:");
441 for (ContentProviderOperation operation : diff) {
442 Log.v(TAG, operation.toString());
443 }
444 }
445
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700446 int numberProcessed = 0;
447 boolean batchFailed = false;
448 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
449 while (numberProcessed < diff.size()) {
450 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
451 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700452 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700453 batchFailed = true;
454 break;
455 } else {
456 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700457 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800458 }
459
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700460 if (batchFailed) {
461 // Retry save
462 continue;
463 }
464
Wenyi Wang67addcc2015-11-23 10:07:48 -0800465 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800466 if (rawContactId == -1) {
467 throw new IllegalStateException("Could not determine RawContact ID after save");
468 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800469 // We don't have to check to see if the value is still -1. If we reach here,
470 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800471 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700472 if (isProfile) {
473 // Since the profile supports local raw contacts, which may have been completely
474 // removed if all information was removed, we need to do a special query to
475 // get the lookup URI for the profile contact (if it still exists).
476 Cursor c = resolver.query(Profile.CONTENT_URI,
477 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
478 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800479 if (c == null) {
480 continue;
481 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700482 try {
Erik162b7e32011-09-20 15:23:55 -0700483 if (c.moveToFirst()) {
484 final long contactId = c.getLong(0);
485 final String lookupKey = c.getString(1);
486 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
487 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700488 } finally {
489 c.close();
490 }
491 } else {
492 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
493 rawContactId);
494 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
495 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800496 if (lookupUri != null) {
497 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
498 }
Josh Garguse692e012012-01-18 14:53:11 -0800499
500 // We can change this back to false later, if we fail to save the contact photo.
501 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800502 break;
503
504 } catch (RemoteException e) {
505 // Something went wrong, bail without success
506 Log.e(TAG, "Problem persisting user edits", e);
507 break;
508
Jay Shrauner57fca182014-01-17 14:20:50 -0800509 } catch (IllegalArgumentException e) {
510 // This is thrown by applyBatch on malformed requests
511 Log.e(TAG, "Problem persisting user edits", e);
512 showToast(R.string.contactSavedErrorToast);
513 break;
514
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800515 } catch (OperationApplicationException e) {
516 // Version consistency failed, re-parent change and try again
517 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
518 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
519 boolean first = true;
520 final int count = state.size();
521 for (int i = 0; i < count; i++) {
522 Long rawContactId = state.getRawContactId(i);
523 if (rawContactId != null && rawContactId != -1) {
524 if (!first) {
525 sb.append(',');
526 }
527 sb.append(rawContactId);
528 first = false;
529 }
530 }
531 sb.append(")");
532
533 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800534 throw new IllegalStateException(
535 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800536 }
537
Maurice Chu851222a2012-06-21 11:43:08 -0700538 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700539 isProfile
540 ? RawContactsEntity.PROFILE_CONTENT_URI
541 : RawContactsEntity.CONTENT_URI,
542 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700543 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700544
545 // Update the new state to use profile URIs if appropriate.
546 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700547 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700548 delta.setProfileQueryUri();
549 }
550 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800551 }
552 }
553
Josh Garguse692e012012-01-18 14:53:11 -0800554 // Now save any updated photos. We do this at the end to ensure that
555 // the ContactProvider already knows about newly-created contacts.
556 if (updatedPhotos != null) {
557 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700558 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800559 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800560
561 // If the raw-contact ID is negative, we are saving a new raw-contact;
562 // replace the bogus ID with the new one that we actually saved the contact at.
563 if (rawContactId < 0) {
564 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800565 }
566
Jay Shrauner511561d2015-04-02 10:35:33 -0700567 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700568 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700569 succeeded = false;
570 }
Josh Garguse692e012012-01-18 14:53:11 -0800571 }
572 }
573
Josh Garguse5d3f892012-04-11 11:56:15 -0700574 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
575 if (callbackIntent != null) {
576 if (succeeded) {
577 // Mark the intent to indicate that the save was successful (even if the lookup URI
578 // is now null). For local contacts or the local profile, it's possible that the
579 // save triggered removal of the contact, so no lookup URI would exist..
580 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
581 }
582 callbackIntent.setData(lookupUri);
583 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800584 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800585 }
586
Josh Garguse692e012012-01-18 14:53:11 -0800587 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700588 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
589 * subsets, adds the returned array to "results".
590 *
591 * @return the size of the array, if not null; -1 when the array is null.
592 */
593 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
594 ContentProviderResult[] results, ContentResolver resolver)
595 throws RemoteException, OperationApplicationException {
596 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
597 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
598 subset.addAll(diff.subList(offset, offset + subsetCount));
599 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
600 .AUTHORITY, subset);
601 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
602 return -1;
603 }
604 for (ContentProviderResult c : subsetResult) {
605 results[offset++] = c;
606 }
607 return subsetResult.length;
608 }
609
610 /**
Josh Garguse692e012012-01-18 14:53:11 -0800611 * Save updated photo for the specified raw-contact.
612 * @return true for success, false for failure
613 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800614 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800615 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800616 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
617 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
618
benny.lin3a4e7a22014-01-08 10:58:08 +0800619 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800620 }
621
Josh Gargusef15c8e2012-01-30 16:42:02 -0800622 /**
623 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
624 */
Maurice Chu851222a2012-06-21 11:43:08 -0700625 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800626 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800627 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800628 long existingRawContactId = state.findRawContactId();
629 if (existingRawContactId != -1) {
630 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800631 }
632
Wenyi Wang67addcc2015-11-23 10:07:48 -0800633 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800634 }
635
636 /**
637 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
638 */
639 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800640 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800641 if (results == null) {
642 return -1;
643 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800644 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800645 final int numResults = results.length;
646 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800647 final CPOWrapper cpoWrapper = diffWrapper.get(i);
648 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
649 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
650 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800651 return ContentUris.parseId(results[i].uri);
652 }
653 }
654 return -1;
655 }
656
657 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700658 * Creates an intent that can be sent to this service to create a new group as
659 * well as add new members at the same time.
660 *
661 * @param context of the application
662 * @param account in which the group should be created
663 * @param label is the name of the group (cannot be null)
664 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
665 * should be added to the group
666 * @param callbackActivity is the activity to send the callback intent to
667 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700668 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700669 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700670 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700671 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800672 Intent serviceIntent = new Intent(context, ContactSaveService.class);
673 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
674 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
675 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700676 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800677 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700678 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700679
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800680 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700681 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800682 Intent callbackIntent = new Intent(context, callbackActivity);
683 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700684 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800685
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700686 return serviceIntent;
687 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800688
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800689 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700690 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
691 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
692 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
693 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700694 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800695
696 ContentValues values = new ContentValues();
697 values.put(Groups.ACCOUNT_TYPE, accountType);
698 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700699 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800700 values.put(Groups.TITLE, label);
701
Katherine Kuan717e3432011-07-13 17:03:24 -0700702 final ContentResolver resolver = getContentResolver();
703
704 // Create the new group
705 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
706
707 // If there's no URI, then the insertion failed. Abort early because group members can't be
708 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800709 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700710 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800711 return;
712 }
713
Katherine Kuan717e3432011-07-13 17:03:24 -0700714 // Add new group members
715 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
716
717 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
718 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800719 values.clear();
720 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
721 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
722
723 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700724 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700725 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800726 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800727 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800728 }
729
730 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800731 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800732 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700733 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700734 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800735 Intent serviceIntent = new Intent(context, ContactSaveService.class);
736 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
737 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
738 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700739
740 // Callback intent will be invoked by the service once the group is renamed.
741 Intent callbackIntent = new Intent(context, callbackActivity);
742 callbackIntent.setAction(callbackAction);
743 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
744
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800745 return serviceIntent;
746 }
747
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800748 private void renameGroup(Intent intent) {
749 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
750 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
751
752 if (groupId == -1) {
753 Log.e(TAG, "Invalid arguments for renameGroup request");
754 return;
755 }
756
757 ContentValues values = new ContentValues();
758 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700759 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
760 getContentResolver().update(groupUri, values, null, null);
761
762 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
763 callbackIntent.setData(groupUri);
764 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800765 }
766
767 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800768 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800769 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800770 public static Intent createGroupDeletionIntent(Context context, long groupId) {
771 Intent serviceIntent = new Intent(context, ContactSaveService.class);
772 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800773 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800774 return serviceIntent;
775 }
776
777 private void deleteGroup(Intent intent) {
778 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
779 if (groupId == -1) {
780 Log.e(TAG, "Invalid arguments for deleteGroup request");
781 return;
782 }
783
784 getContentResolver().delete(
785 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
786 }
787
788 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700789 * Creates an intent that can be sent to this service to rename a group as
790 * well as add and remove members from the group.
791 *
792 * @param context of the application
793 * @param groupId of the group that should be modified
794 * @param newLabel is the updated name of the group (can be null if the name
795 * should not be updated)
796 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
797 * should be added to the group
798 * @param rawContactsToRemove is an array of raw contact IDs for contacts
799 * that should be removed from the group
800 * @param callbackActivity is the activity to send the callback intent to
801 * @param callbackAction is the intent action for the callback intent
802 */
803 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
804 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700805 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700806 Intent serviceIntent = new Intent(context, ContactSaveService.class);
807 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
808 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
809 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
810 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
811 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
812 rawContactsToRemove);
813
814 // Callback intent will be invoked by the service once the group is updated
815 Intent callbackIntent = new Intent(context, callbackActivity);
816 callbackIntent.setAction(callbackAction);
817 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
818
819 return serviceIntent;
820 }
821
822 private void updateGroup(Intent intent) {
823 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
824 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
825 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
826 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
827
828 if (groupId == -1) {
829 Log.e(TAG, "Invalid arguments for updateGroup request");
830 return;
831 }
832
833 final ContentResolver resolver = getContentResolver();
834 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
835
836 // Update group name if necessary
837 if (label != null) {
838 ContentValues values = new ContentValues();
839 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700840 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700841 }
842
Katherine Kuan717e3432011-07-13 17:03:24 -0700843 // Add and remove members if necessary
844 addMembersToGroup(resolver, rawContactsToAdd, groupId);
845 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
846
847 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
848 callbackIntent.setData(groupUri);
849 deliverCallback(callbackIntent);
850 }
851
Daniel Lehmann18958a22012-02-28 17:45:25 -0800852 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700853 long groupId) {
854 if (rawContactsToAdd == null) {
855 return;
856 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700857 for (long rawContactId : rawContactsToAdd) {
858 try {
859 final ArrayList<ContentProviderOperation> rawContactOperations =
860 new ArrayList<ContentProviderOperation>();
861
862 // Build an assert operation to ensure the contact is not already in the group
863 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
864 .newAssertQuery(Data.CONTENT_URI);
865 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
866 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
867 new String[] { String.valueOf(rawContactId),
868 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
869 assertBuilder.withExpectedCount(0);
870 rawContactOperations.add(assertBuilder.build());
871
872 // Build an insert operation to add the contact to the group
873 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
874 .newInsert(Data.CONTENT_URI);
875 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
876 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
877 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
878 rawContactOperations.add(insertBuilder.build());
879
880 if (DEBUG) {
881 for (ContentProviderOperation operation : rawContactOperations) {
882 Log.v(TAG, operation.toString());
883 }
884 }
885
886 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700887 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800888 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700889 }
890 } catch (RemoteException e) {
891 // Something went wrong, bail without success
892 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
893 String.valueOf(rawContactId), e);
894 } catch (OperationApplicationException e) {
895 // The assert could have failed because the contact is already in the group,
896 // just continue to the next contact
897 Log.w(TAG, "Assert failed in adding raw contact ID " +
898 String.valueOf(rawContactId) + ". Already exists in group " +
899 String.valueOf(groupId), e);
900 }
901 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700902 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700903
Daniel Lehmann18958a22012-02-28 17:45:25 -0800904 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700905 long groupId) {
906 if (rawContactsToRemove == null) {
907 return;
908 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700909 for (long rawContactId : rawContactsToRemove) {
910 // Apply the delete operation on the data row for the given raw contact's
911 // membership in the given group. If no contact matches the provided selection, then
912 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800913 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700914 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
915 new String[] { String.valueOf(rawContactId),
916 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
917 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700918 }
919
920 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800921 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800922 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800923 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
924 Intent serviceIntent = new Intent(context, ContactSaveService.class);
925 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
926 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
927 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
928
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800929 return serviceIntent;
930 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800931
932 private void setStarred(Intent intent) {
933 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
934 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
935 if (contactUri == null) {
936 Log.e(TAG, "Invalid arguments for setStarred request");
937 return;
938 }
939
940 final ContentValues values = new ContentValues(1);
941 values.put(Contacts.STARRED, value);
942 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700943
944 // Undemote the contact if necessary
945 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
946 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800947 if (c == null) {
948 return;
949 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700950 try {
951 if (c.moveToFirst()) {
952 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700953
954 // Don't bother undemoting if this contact is the user's profile.
955 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800956 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700957 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700958 }
959 } finally {
960 c.close();
961 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800962 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800963
964 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700965 * Creates an intent that can be sent to this service to set the redirect to voicemail.
966 */
967 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
968 boolean value) {
969 Intent serviceIntent = new Intent(context, ContactSaveService.class);
970 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
973
974 return serviceIntent;
975 }
976
977 private void setSendToVoicemail(Intent intent) {
978 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
979 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
980 if (contactUri == null) {
981 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
982 return;
983 }
984
985 final ContentValues values = new ContentValues(1);
986 values.put(Contacts.SEND_TO_VOICEMAIL, value);
987 getContentResolver().update(contactUri, values, null, null);
988 }
989
990 /**
991 * Creates an intent that can be sent to this service to save the contact's ringtone.
992 */
993 public static Intent createSetRingtone(Context context, Uri contactUri,
994 String value) {
995 Intent serviceIntent = new Intent(context, ContactSaveService.class);
996 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
997 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
998 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
999
1000 return serviceIntent;
1001 }
1002
1003 private void setRingtone(Intent intent) {
1004 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1005 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1006 if (contactUri == null) {
1007 Log.e(TAG, "Invalid arguments for setRingtone");
1008 return;
1009 }
1010 ContentValues values = new ContentValues(1);
1011 values.put(Contacts.CUSTOM_RINGTONE, value);
1012 getContentResolver().update(contactUri, values, null, null);
1013 }
1014
1015 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001016 * Creates an intent that sets the selected data item as super primary (default)
1017 */
1018 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1019 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1020 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1021 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1022 return serviceIntent;
1023 }
1024
1025 private void setSuperPrimary(Intent intent) {
1026 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1027 if (dataId == -1) {
1028 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1029 return;
1030 }
1031
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001032 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001033 }
1034
1035 /**
1036 * Creates an intent that clears the primary flag of all data items that belong to the same
1037 * raw_contact as the given data item. Will only clear, if the data item was primary before
1038 * this call
1039 */
1040 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1041 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1042 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1043 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1044 return serviceIntent;
1045 }
1046
1047 private void clearPrimary(Intent intent) {
1048 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1049 if (dataId == -1) {
1050 Log.e(TAG, "Invalid arguments for clearPrimary request");
1051 return;
1052 }
1053
1054 // Update the primary values in the data record.
1055 ContentValues values = new ContentValues(1);
1056 values.put(Data.IS_SUPER_PRIMARY, 0);
1057 values.put(Data.IS_PRIMARY, 0);
1058
1059 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1060 values, null, null);
1061 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001062
1063 /**
1064 * Creates an intent that can be sent to this service to delete a contact.
1065 */
1066 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1067 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1068 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1069 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1070 return serviceIntent;
1071 }
1072
Brian Attwelld2962a32015-03-02 14:48:50 -08001073 /**
1074 * Creates an intent that can be sent to this service to delete multiple contacts.
1075 */
1076 public static Intent createDeleteMultipleContactsIntent(Context context,
1077 long[] contactIds) {
1078 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1079 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1080 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1081 return serviceIntent;
1082 }
1083
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001084 private void deleteContact(Intent intent) {
1085 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1086 if (contactUri == null) {
1087 Log.e(TAG, "Invalid arguments for deleteContact request");
1088 return;
1089 }
1090
1091 getContentResolver().delete(contactUri, null, null);
1092 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001093
Brian Attwelld2962a32015-03-02 14:48:50 -08001094 private void deleteMultipleContacts(Intent intent) {
1095 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1096 if (contactIds == null) {
1097 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1098 return;
1099 }
1100 for (long contactId : contactIds) {
1101 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1102 getContentResolver().delete(contactUri, null, null);
1103 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001104 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1105 .contacts_deleted_toast, contactIds.length);
1106 mMainHandler.post(new Runnable() {
1107 @Override
1108 public void run() {
1109 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1110 .show();
1111 }
1112 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001113 }
1114
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001115 /**
1116 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001117 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001118 */
1119 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001120 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001121 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1122 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1123 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1124 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001125
1126 // Callback intent will be invoked by the service once the contacts are joined.
1127 Intent callbackIntent = new Intent(context, callbackActivity);
1128 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001129 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1130
1131 return serviceIntent;
1132 }
1133
Brian Attwelld3946ca2015-03-03 11:13:49 -08001134 /**
1135 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1136 * No special attention is paid to where the resulting contact's name is taken from.
1137 */
1138 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1139 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1140 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1141 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1142 return serviceIntent;
1143 }
1144
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145
1146 private interface JoinContactQuery {
1147 String[] PROJECTION = {
1148 RawContacts._ID,
1149 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001150 RawContacts.DISPLAY_NAME_SOURCE,
1151 };
1152
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001153 int _ID = 0;
1154 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001155 int DISPLAY_NAME_SOURCE = 2;
1156 }
1157
1158 private interface ContactEntityQuery {
1159 String[] PROJECTION = {
1160 Contacts.Entity.DATA_ID,
1161 Contacts.Entity.CONTACT_ID,
1162 Contacts.Entity.IS_SUPER_PRIMARY,
1163 };
1164 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1165 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1166 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1167 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1168
1169 int DATA_ID = 0;
1170 int CONTACT_ID = 1;
1171 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001172 }
1173
Brian Attwelld3946ca2015-03-03 11:13:49 -08001174 private void joinSeveralContacts(Intent intent) {
1175 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001176
Brian Attwelld3946ca2015-03-03 11:13:49 -08001177 // Load raw contact IDs for all contacts involved.
1178 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1179 if (rawContactIds == null) {
1180 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001181 return;
1182 }
1183
Brian Attwelld3946ca2015-03-03 11:13:49 -08001184 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001185 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001186 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1187 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1188 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001189 for (int i = 0; i < rawContactIds.length; i++) {
1190 for (int j = 0; j < rawContactIds.length; j++) {
1191 if (i != j) {
1192 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1193 }
Walter Jang0653de32015-07-24 12:12:40 -07001194 // Before we get to 500 we need to flush the operations list
1195 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1196 if (!applyJoinOperations(resolver, operations)) {
1197 return;
1198 }
1199 operations.clear();
1200 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001201 }
1202 }
Walter Jang0653de32015-07-24 12:12:40 -07001203 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1204 return;
1205 }
1206 showToast(R.string.contactsJoinedMessage);
1207 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001208
Walter Jang0653de32015-07-24 12:12:40 -07001209 /** Returns true if the batch was successfully applied and false otherwise. */
1210 private boolean applyJoinOperations(ContentResolver resolver,
1211 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001212 try {
1213 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001214 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001215 } catch (RemoteException | OperationApplicationException e) {
1216 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1217 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001218 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001219 }
1220 }
1221
1222
1223 private void joinContacts(Intent intent) {
1224 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1225 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001226
1227 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001228 // in the join UIs.
1229 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1230 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001231 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001232 return;
1233 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001234
Brian Attwell548f5c62015-01-27 17:46:46 -08001235 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001236
1237 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001238 for (int i = 0; i < rawContactIds.length; i++) {
1239 for (int j = 0; j < rawContactIds.length; j++) {
1240 if (i != j) {
1241 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1242 }
1243 }
1244 }
1245
Brian Attwelld3946ca2015-03-03 11:13:49 -08001246 final ContentResolver resolver = getContentResolver();
1247
Brian Attwell548f5c62015-01-27 17:46:46 -08001248 // Use the name for contactId1 as the name for the newly aggregated contact.
1249 final Uri contactId1Uri = ContentUris.withAppendedId(
1250 Contacts.CONTENT_URI, contactId1);
1251 final Uri entityUri = Uri.withAppendedPath(
1252 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1253 Cursor c = resolver.query(entityUri,
1254 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1255 if (c == null) {
1256 Log.e(TAG, "Unable to open Contacts DB cursor");
1257 showToast(R.string.contactSavedErrorToast);
1258 return;
1259 }
1260 long dataIdToAddSuperPrimary = -1;
1261 try {
1262 if (c.moveToFirst()) {
1263 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1264 }
1265 } finally {
1266 c.close();
1267 }
1268
1269 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1270 // display name does not change as a result of the join.
1271 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001272 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001273 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1274 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1275 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001276 operations.add(builder.build());
1277 }
1278
1279 boolean success = false;
1280 // Apply all aggregation exceptions as one batch
1281 try {
1282 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001283 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001284 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001285 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001286 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001287 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001288 }
1289
1290 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1291 if (success) {
1292 Uri uri = RawContacts.getContactLookupUri(resolver,
1293 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1294 callbackIntent.setData(uri);
1295 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001296 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 }
1298
Brian Attwelld3946ca2015-03-03 11:13:49 -08001299 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1300 if (contactIds == null) {
1301 return null;
1302 }
1303
Brian Attwell548f5c62015-01-27 17:46:46 -08001304 final ContentResolver resolver = getContentResolver();
1305 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001306
1307 final StringBuilder queryBuilder = new StringBuilder();
1308 final String stringContactIds[] = new String[contactIds.length];
1309 for (int i = 0; i < contactIds.length; i++) {
1310 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1311 stringContactIds[i] = String.valueOf(contactIds[i]);
1312 if (contactIds[i] == -1) {
1313 return null;
1314 }
1315 if (i == contactIds.length -1) {
1316 break;
1317 }
1318 queryBuilder.append(" OR ");
1319 }
1320
Brian Attwell548f5c62015-01-27 17:46:46 -08001321 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1322 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001323 queryBuilder.toString(),
1324 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001325 if (c == null) {
1326 Log.e(TAG, "Unable to open Contacts DB cursor");
1327 showToast(R.string.contactSavedErrorToast);
1328 return null;
1329 }
1330 try {
1331 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001332 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001333 return null;
1334 }
1335 rawContactIds = new long[c.getCount()];
1336 for (int i = 0; i < rawContactIds.length; i++) {
1337 c.moveToPosition(i);
1338 long rawContactId = c.getLong(JoinContactQuery._ID);
1339 rawContactIds[i] = rawContactId;
1340 }
1341 } finally {
1342 c.close();
1343 }
1344 return rawContactIds;
1345 }
1346
Brian Attwelld3946ca2015-03-03 11:13:49 -08001347 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1348 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1349 }
1350
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001351 /**
1352 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1353 */
1354 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1355 long rawContactId1, long rawContactId2) {
1356 Builder builder =
1357 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1358 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1359 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1360 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1361 operations.add(builder.build());
1362 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001363
1364 /**
1365 * Shows a toast on the UI thread.
1366 */
1367 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001368 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001369
1370 @Override
1371 public void run() {
1372 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1373 }
1374 });
1375 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001376
1377 private void deliverCallback(final Intent callbackIntent) {
1378 mMainHandler.post(new Runnable() {
1379
1380 @Override
1381 public void run() {
1382 deliverCallbackOnUiThread(callbackIntent);
1383 }
1384 });
1385 }
1386
1387 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1388 // TODO: this assumes that if there are multiple instances of the same
1389 // activity registered, the last one registered is the one waiting for
1390 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001391 for (Listener listener : sListeners) {
1392 if (callbackIntent.getComponent().equals(
1393 ((Activity) listener).getIntent().getComponent())) {
1394 listener.onServiceCompleted(callbackIntent);
1395 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001396 }
1397 }
1398 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001399}