blob: 08eb1c7794166cffd7fa9b7c3ad194124b416622 [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;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080050import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051
Wenyi Wangdd7d4562015-12-08 13:33:43 -080052import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080053import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070054import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080055import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080056import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070057import com.android.contacts.common.model.RawContactDelta;
58import com.android.contacts.common.model.RawContactDeltaList;
59import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080060import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070061import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080062import com.android.contacts.compat.PinnedPositionsCompat;
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";
Gary Mai7efa9942016-05-12 11:26:49 -070089 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
90 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070091
Dmitri Plotnikova0114142011-02-15 13:53:21 -080092 public static final String ACTION_SAVE_CONTACT = "saveContact";
93 public static final String EXTRA_CONTACT_STATE = "state";
94 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070095 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070096 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080097 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070098
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080099 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100 public static final String ACTION_RENAME_GROUP = "renameGroup";
101 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700102 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800103 public static final String EXTRA_GROUP_ID = "groupId";
104 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700105 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
106 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800107
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800108 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800109 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800110 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800111 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800112 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800113 public static final String EXTRA_STARRED_FLAG = "starred";
114
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800115 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
116 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
117 public static final String EXTRA_DATA_ID = "dataId";
118
Gary Mai7efa9942016-05-12 11:26:49 -0700119 public static final String ACTION_SPLIT_CONTACT = "splitContact";
120
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800121 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800122 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800123 public static final String EXTRA_CONTACT_ID1 = "contactId1";
124 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800125
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700126 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
127 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
128
129 public static final String ACTION_SET_RINGTONE = "setRingtone";
130 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
131
Gary Mai7efa9942016-05-12 11:26:49 -0700132 public static final int CP2_ERROR = 0;
133 public static final int CONTACTS_LINKED = 1;
134 public static final int CONTACTS_SPLIT = 2;
135
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700136 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
137 Data.MIMETYPE,
138 Data.IS_PRIMARY,
139 Data.DATA1,
140 Data.DATA2,
141 Data.DATA3,
142 Data.DATA4,
143 Data.DATA5,
144 Data.DATA6,
145 Data.DATA7,
146 Data.DATA8,
147 Data.DATA9,
148 Data.DATA10,
149 Data.DATA11,
150 Data.DATA12,
151 Data.DATA13,
152 Data.DATA14,
153 Data.DATA15
154 );
155
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800156 private static final int PERSIST_TRIES = 3;
157
Walter Jang0653de32015-07-24 12:12:40 -0700158 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
159
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800160 public interface Listener {
161 public void onServiceCompleted(Intent callbackIntent);
162 }
163
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100164 private static final CopyOnWriteArrayList<Listener> sListeners =
165 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800166
167 private Handler mMainHandler;
168
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700169 public ContactSaveService() {
170 super(TAG);
171 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800172 mMainHandler = new Handler(Looper.getMainLooper());
173 }
174
175 public static void registerListener(Listener listener) {
176 if (!(listener instanceof Activity)) {
177 throw new ClassCastException("Only activities can be registered to"
178 + " receive callback from " + ContactSaveService.class.getName());
179 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100180 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800181 }
182
183 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100184 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700185 }
186
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800187 /**
188 * Returns true if the ContactSaveService was started successfully and false if an exception
189 * was thrown and a Toast error message was displayed.
190 */
191 public static boolean startService(Context context, Intent intent, int saveMode) {
192 try {
193 context.startService(intent);
194 } catch (Exception exception) {
195 final int resId;
196 switch (saveMode) {
197 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
198 resId = R.string.contactUnlinkErrorToast;
199 break;
200 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
201 resId = R.string.contactJoinErrorToast;
202 break;
203 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
204 resId = R.string.contactSavedErrorToast;
205 break;
206 default:
207 resId = R.string.contactGenericErrorToast;
208 }
209 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
210 return false;
211 }
212 return true;
213 }
214
215 /**
216 * Utility method that starts service and handles exception.
217 */
218 public static void startService(Context context, Intent intent) {
219 try {
220 context.startService(intent);
221 } catch (Exception exception) {
222 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
223 }
224 }
225
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700226 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800227 public Object getSystemService(String name) {
228 Object service = super.getSystemService(name);
229 if (service != null) {
230 return service;
231 }
232
233 return getApplicationContext().getSystemService(name);
234 }
235
236 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700237 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800238 if (intent == null) {
239 Log.d(TAG, "onHandleIntent: could not handle null intent");
240 return;
241 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700242 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
243 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
244 // TODO: add more specific error string such as "Turn on Contacts
245 // permission to update your contacts"
246 showToast(R.string.contactSavedErrorToast);
247 return;
248 }
249
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700250 // Call an appropriate method. If we're sure it affects how incoming phone calls are
251 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700252 String action = intent.getAction();
253 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
254 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800255 } else if (ACTION_SAVE_CONTACT.equals(action)) {
256 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800257 } else if (ACTION_CREATE_GROUP.equals(action)) {
258 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800259 } else if (ACTION_RENAME_GROUP.equals(action)) {
260 renameGroup(intent);
261 } else if (ACTION_DELETE_GROUP.equals(action)) {
262 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700263 } else if (ACTION_UPDATE_GROUP.equals(action)) {
264 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800265 } else if (ACTION_SET_STARRED.equals(action)) {
266 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800267 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
268 setSuperPrimary(intent);
269 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
270 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800271 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
272 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800273 } else if (ACTION_DELETE_CONTACT.equals(action)) {
274 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700275 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
276 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800277 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
278 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800279 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
280 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700281 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
282 setSendToVoicemail(intent);
283 } else if (ACTION_SET_RINGTONE.equals(action)) {
284 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700285 }
286 }
287
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800288 /**
289 * Creates an intent that can be sent to this service to create a new raw contact
290 * using data presented as a set of ContentValues.
291 */
292 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700293 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700294 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800295 Intent serviceIntent = new Intent(
296 context, ContactSaveService.class);
297 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
298 if (account != null) {
299 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
300 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700301 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800302 }
303 serviceIntent.putParcelableArrayListExtra(
304 ContactSaveService.EXTRA_CONTENT_VALUES, values);
305
306 // Callback intent will be invoked by the service once the new contact is
307 // created. The service will put the URI of the new contact as "data" on
308 // the callback intent.
309 Intent callbackIntent = new Intent(context, callbackActivity);
310 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800311 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
312 return serviceIntent;
313 }
314
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700315 private void createRawContact(Intent intent) {
316 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
317 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700318 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700319 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
320 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
321
322 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
323 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
324 .withValue(RawContacts.ACCOUNT_NAME, accountName)
325 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700326 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700327 .build());
328
329 int size = valueList.size();
330 for (int i = 0; i < size; i++) {
331 ContentValues values = valueList.get(i);
332 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
333 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
334 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
335 .withValues(values)
336 .build());
337 }
338
339 ContentResolver resolver = getContentResolver();
340 ContentProviderResult[] results;
341 try {
342 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
343 } catch (Exception e) {
344 throw new RuntimeException("Failed to store new contact", e);
345 }
346
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700347 Uri rawContactUri = results[0].uri;
348 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
349
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800350 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700351 }
352
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700353 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354 * Creates an intent that can be sent to this service to create a new raw contact
355 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800356 * This variant is more convenient to use when there is only one photo that can
357 * possibly be updated, as in the Contact Details screen.
358 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
359 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800360 */
Maurice Chu851222a2012-06-21 11:43:08 -0700361 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700362 String saveModeExtraKey, int saveMode, boolean isProfile,
363 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700364 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800365 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700366 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800367 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700368 callbackActivity, callbackAction, bundle,
369 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800370 }
371
372 /**
373 * Creates an intent that can be sent to this service to create a new raw contact
374 * using data presented as a set of ContentValues.
375 * This variant is used when multiple contacts' photos may be updated, as in the
376 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700377 *
Josh Garguse692e012012-01-18 14:53:11 -0800378 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700379 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
380 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800381 */
Maurice Chu851222a2012-06-21 11:43:08 -0700382 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700383 String saveModeExtraKey, int saveMode, boolean isProfile,
384 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700385 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000386 Intent serviceIntent = new Intent(
387 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800388 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
389 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700390 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800391 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
392
Josh Garguse692e012012-01-18 14:53:11 -0800393 if (updatedPhotos != null) {
394 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
395 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800396
Josh Garguse5d3f892012-04-11 11:56:15 -0700397 if (callbackActivity != null) {
398 // Callback intent will be invoked by the service once the contact is
399 // saved. The service will put the URI of the new contact as "data" on
400 // the callback intent.
401 Intent callbackIntent = new Intent(context, callbackActivity);
402 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700403 if (joinContactIdExtraKey != null && joinContactId != null) {
404 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
405 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700406 callbackIntent.setAction(callbackAction);
407 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
408 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800409 return serviceIntent;
410 }
411
412 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700413 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700414 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800415 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800416
Jay Shrauner08099782015-03-25 14:17:11 -0700417 if (state == null) {
418 Log.e(TAG, "Invalid arguments for saveContact request");
419 return;
420 }
421
benny.lin3a4e7a22014-01-08 10:58:08 +0800422 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800423 // Trim any empty fields, and RawContacts, before persisting
424 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700425 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800426
427 Uri lookupUri = null;
428
429 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700430
Josh Garguse692e012012-01-18 14:53:11 -0800431 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800432
Josh Gargusef15c8e2012-01-30 16:42:02 -0800433 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
434 long insertedRawContactId = -1;
435
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800436 // Attempt to persist changes
437 int tries = 0;
438 while (tries++ < PERSIST_TRIES) {
439 try {
440 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800441 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
442
443 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
444
445 for (CPOWrapper cpoWrapper : diffWrapper) {
446 diff.add(cpoWrapper.getOperation());
447 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700448
Katherine Kuana007e442011-07-07 09:25:34 -0700449 if (DEBUG) {
450 Log.v(TAG, "Content Provider Operations:");
451 for (ContentProviderOperation operation : diff) {
452 Log.v(TAG, operation.toString());
453 }
454 }
455
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700456 int numberProcessed = 0;
457 boolean batchFailed = false;
458 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
459 while (numberProcessed < diff.size()) {
460 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
461 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700462 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700463 batchFailed = true;
464 break;
465 } else {
466 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700467 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800468 }
469
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700470 if (batchFailed) {
471 // Retry save
472 continue;
473 }
474
Wenyi Wang67addcc2015-11-23 10:07:48 -0800475 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800476 if (rawContactId == -1) {
477 throw new IllegalStateException("Could not determine RawContact ID after save");
478 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800479 // We don't have to check to see if the value is still -1. If we reach here,
480 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800481 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700482 if (isProfile) {
483 // Since the profile supports local raw contacts, which may have been completely
484 // removed if all information was removed, we need to do a special query to
485 // get the lookup URI for the profile contact (if it still exists).
486 Cursor c = resolver.query(Profile.CONTENT_URI,
487 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
488 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800489 if (c == null) {
490 continue;
491 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700492 try {
Erik162b7e32011-09-20 15:23:55 -0700493 if (c.moveToFirst()) {
494 final long contactId = c.getLong(0);
495 final String lookupKey = c.getString(1);
496 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
497 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700498 } finally {
499 c.close();
500 }
501 } else {
502 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
503 rawContactId);
504 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
505 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800506 if (lookupUri != null) {
507 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
508 }
Josh Garguse692e012012-01-18 14:53:11 -0800509
510 // We can change this back to false later, if we fail to save the contact photo.
511 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512 break;
513
514 } catch (RemoteException e) {
515 // Something went wrong, bail without success
516 Log.e(TAG, "Problem persisting user edits", e);
517 break;
518
Jay Shrauner57fca182014-01-17 14:20:50 -0800519 } catch (IllegalArgumentException e) {
520 // This is thrown by applyBatch on malformed requests
521 Log.e(TAG, "Problem persisting user edits", e);
522 showToast(R.string.contactSavedErrorToast);
523 break;
524
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800525 } catch (OperationApplicationException e) {
526 // Version consistency failed, re-parent change and try again
527 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
528 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
529 boolean first = true;
530 final int count = state.size();
531 for (int i = 0; i < count; i++) {
532 Long rawContactId = state.getRawContactId(i);
533 if (rawContactId != null && rawContactId != -1) {
534 if (!first) {
535 sb.append(',');
536 }
537 sb.append(rawContactId);
538 first = false;
539 }
540 }
541 sb.append(")");
542
543 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800544 throw new IllegalStateException(
545 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800546 }
547
Maurice Chu851222a2012-06-21 11:43:08 -0700548 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700549 isProfile
550 ? RawContactsEntity.PROFILE_CONTENT_URI
551 : RawContactsEntity.CONTENT_URI,
552 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700553 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700554
555 // Update the new state to use profile URIs if appropriate.
556 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700557 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700558 delta.setProfileQueryUri();
559 }
560 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800561 }
562 }
563
Josh Garguse692e012012-01-18 14:53:11 -0800564 // Now save any updated photos. We do this at the end to ensure that
565 // the ContactProvider already knows about newly-created contacts.
566 if (updatedPhotos != null) {
567 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700568 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800569 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800570
571 // If the raw-contact ID is negative, we are saving a new raw-contact;
572 // replace the bogus ID with the new one that we actually saved the contact at.
573 if (rawContactId < 0) {
574 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800575 }
576
Jay Shrauner511561d2015-04-02 10:35:33 -0700577 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700578 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700579 succeeded = false;
580 }
Josh Garguse692e012012-01-18 14:53:11 -0800581 }
582 }
583
Josh Garguse5d3f892012-04-11 11:56:15 -0700584 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
585 if (callbackIntent != null) {
586 if (succeeded) {
587 // Mark the intent to indicate that the save was successful (even if the lookup URI
588 // is now null). For local contacts or the local profile, it's possible that the
589 // save triggered removal of the contact, so no lookup URI would exist..
590 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
591 }
592 callbackIntent.setData(lookupUri);
593 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800594 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800595 }
596
Josh Garguse692e012012-01-18 14:53:11 -0800597 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700598 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
599 * subsets, adds the returned array to "results".
600 *
601 * @return the size of the array, if not null; -1 when the array is null.
602 */
603 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
604 ContentProviderResult[] results, ContentResolver resolver)
605 throws RemoteException, OperationApplicationException {
606 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
607 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
608 subset.addAll(diff.subList(offset, offset + subsetCount));
609 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
610 .AUTHORITY, subset);
611 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
612 return -1;
613 }
614 for (ContentProviderResult c : subsetResult) {
615 results[offset++] = c;
616 }
617 return subsetResult.length;
618 }
619
620 /**
Josh Garguse692e012012-01-18 14:53:11 -0800621 * Save updated photo for the specified raw-contact.
622 * @return true for success, false for failure
623 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800624 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800625 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800626 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
627 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
628
benny.lin3a4e7a22014-01-08 10:58:08 +0800629 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800630 }
631
Josh Gargusef15c8e2012-01-30 16:42:02 -0800632 /**
633 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
634 */
Maurice Chu851222a2012-06-21 11:43:08 -0700635 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800636 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800637 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800638 long existingRawContactId = state.findRawContactId();
639 if (existingRawContactId != -1) {
640 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800641 }
642
Wenyi Wang67addcc2015-11-23 10:07:48 -0800643 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800644 }
645
646 /**
647 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
648 */
649 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800650 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800651 if (results == null) {
652 return -1;
653 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800654 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800655 final int numResults = results.length;
656 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800657 final CPOWrapper cpoWrapper = diffWrapper.get(i);
658 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
659 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
660 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800661 return ContentUris.parseId(results[i].uri);
662 }
663 }
664 return -1;
665 }
666
667 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700668 * Creates an intent that can be sent to this service to create a new group as
669 * well as add new members at the same time.
670 *
671 * @param context of the application
672 * @param account in which the group should be created
673 * @param label is the name of the group (cannot be null)
674 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
675 * should be added to the group
676 * @param callbackActivity is the activity to send the callback intent to
677 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700678 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700679 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700680 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700681 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800682 Intent serviceIntent = new Intent(context, ContactSaveService.class);
683 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
684 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
685 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700686 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800687 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700688 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700689
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800690 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700691 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800692 Intent callbackIntent = new Intent(context, callbackActivity);
693 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700694 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800695
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700696 return serviceIntent;
697 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800698
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800699 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700700 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
701 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
702 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
703 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700704 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800705
706 ContentValues values = new ContentValues();
707 values.put(Groups.ACCOUNT_TYPE, accountType);
708 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700709 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800710 values.put(Groups.TITLE, label);
711
Katherine Kuan717e3432011-07-13 17:03:24 -0700712 final ContentResolver resolver = getContentResolver();
713
714 // Create the new group
715 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
716
717 // If there's no URI, then the insertion failed. Abort early because group members can't be
718 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800719 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700720 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800721 return;
722 }
723
Katherine Kuan717e3432011-07-13 17:03:24 -0700724 // Add new group members
725 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
726
727 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
728 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800729 values.clear();
730 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
731 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
732
733 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700734 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800736 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800737 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800738 }
739
740 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800741 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800742 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700743 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700744 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800745 Intent serviceIntent = new Intent(context, ContactSaveService.class);
746 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
747 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
748 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700749
750 // Callback intent will be invoked by the service once the group is renamed.
751 Intent callbackIntent = new Intent(context, callbackActivity);
752 callbackIntent.setAction(callbackAction);
753 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
754
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800755 return serviceIntent;
756 }
757
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800758 private void renameGroup(Intent intent) {
759 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
760 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
761
762 if (groupId == -1) {
763 Log.e(TAG, "Invalid arguments for renameGroup request");
764 return;
765 }
766
767 ContentValues values = new ContentValues();
768 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700769 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
770 getContentResolver().update(groupUri, values, null, null);
771
772 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
773 callbackIntent.setData(groupUri);
774 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800775 }
776
777 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800778 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800779 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800780 public static Intent createGroupDeletionIntent(Context context, long groupId) {
781 Intent serviceIntent = new Intent(context, ContactSaveService.class);
782 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800783 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800784 return serviceIntent;
785 }
786
787 private void deleteGroup(Intent intent) {
788 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
789 if (groupId == -1) {
790 Log.e(TAG, "Invalid arguments for deleteGroup request");
791 return;
792 }
793
794 getContentResolver().delete(
795 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
796 }
797
798 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700799 * Creates an intent that can be sent to this service to rename a group as
800 * well as add and remove members from the group.
801 *
802 * @param context of the application
803 * @param groupId of the group that should be modified
804 * @param newLabel is the updated name of the group (can be null if the name
805 * should not be updated)
806 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
807 * should be added to the group
808 * @param rawContactsToRemove is an array of raw contact IDs for contacts
809 * that should be removed from the group
810 * @param callbackActivity is the activity to send the callback intent to
811 * @param callbackAction is the intent action for the callback intent
812 */
813 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
814 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700815 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700816 Intent serviceIntent = new Intent(context, ContactSaveService.class);
817 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
818 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
819 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
820 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
821 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
822 rawContactsToRemove);
823
824 // Callback intent will be invoked by the service once the group is updated
825 Intent callbackIntent = new Intent(context, callbackActivity);
826 callbackIntent.setAction(callbackAction);
827 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
828
829 return serviceIntent;
830 }
831
832 private void updateGroup(Intent intent) {
833 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
834 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
835 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
836 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
837
838 if (groupId == -1) {
839 Log.e(TAG, "Invalid arguments for updateGroup request");
840 return;
841 }
842
843 final ContentResolver resolver = getContentResolver();
844 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
845
846 // Update group name if necessary
847 if (label != null) {
848 ContentValues values = new ContentValues();
849 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700850 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700851 }
852
Katherine Kuan717e3432011-07-13 17:03:24 -0700853 // Add and remove members if necessary
854 addMembersToGroup(resolver, rawContactsToAdd, groupId);
855 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
856
857 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
858 callbackIntent.setData(groupUri);
859 deliverCallback(callbackIntent);
860 }
861
Daniel Lehmann18958a22012-02-28 17:45:25 -0800862 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700863 long groupId) {
864 if (rawContactsToAdd == null) {
865 return;
866 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700867 for (long rawContactId : rawContactsToAdd) {
868 try {
869 final ArrayList<ContentProviderOperation> rawContactOperations =
870 new ArrayList<ContentProviderOperation>();
871
872 // Build an assert operation to ensure the contact is not already in the group
873 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
874 .newAssertQuery(Data.CONTENT_URI);
875 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
876 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
877 new String[] { String.valueOf(rawContactId),
878 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
879 assertBuilder.withExpectedCount(0);
880 rawContactOperations.add(assertBuilder.build());
881
882 // Build an insert operation to add the contact to the group
883 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
884 .newInsert(Data.CONTENT_URI);
885 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
886 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
887 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
888 rawContactOperations.add(insertBuilder.build());
889
890 if (DEBUG) {
891 for (ContentProviderOperation operation : rawContactOperations) {
892 Log.v(TAG, operation.toString());
893 }
894 }
895
896 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700897 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800898 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700899 }
900 } catch (RemoteException e) {
901 // Something went wrong, bail without success
902 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
903 String.valueOf(rawContactId), e);
904 } catch (OperationApplicationException e) {
905 // The assert could have failed because the contact is already in the group,
906 // just continue to the next contact
907 Log.w(TAG, "Assert failed in adding raw contact ID " +
908 String.valueOf(rawContactId) + ". Already exists in group " +
909 String.valueOf(groupId), e);
910 }
911 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700912 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700913
Daniel Lehmann18958a22012-02-28 17:45:25 -0800914 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700915 long groupId) {
916 if (rawContactsToRemove == null) {
917 return;
918 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700919 for (long rawContactId : rawContactsToRemove) {
920 // Apply the delete operation on the data row for the given raw contact's
921 // membership in the given group. If no contact matches the provided selection, then
922 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800923 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700924 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
925 new String[] { String.valueOf(rawContactId),
926 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
927 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700928 }
929
930 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800931 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800932 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800933 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
934 Intent serviceIntent = new Intent(context, ContactSaveService.class);
935 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
936 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
937 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
938
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800939 return serviceIntent;
940 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800941
942 private void setStarred(Intent intent) {
943 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
944 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
945 if (contactUri == null) {
946 Log.e(TAG, "Invalid arguments for setStarred request");
947 return;
948 }
949
950 final ContentValues values = new ContentValues(1);
951 values.put(Contacts.STARRED, value);
952 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700953
954 // Undemote the contact if necessary
955 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
956 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800957 if (c == null) {
958 return;
959 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700960 try {
961 if (c.moveToFirst()) {
962 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700963
964 // Don't bother undemoting if this contact is the user's profile.
965 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800966 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700967 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700968 }
969 } finally {
970 c.close();
971 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800972 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800973
974 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700975 * Creates an intent that can be sent to this service to set the redirect to voicemail.
976 */
977 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
978 boolean value) {
979 Intent serviceIntent = new Intent(context, ContactSaveService.class);
980 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
981 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
982 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
983
984 return serviceIntent;
985 }
986
987 private void setSendToVoicemail(Intent intent) {
988 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
989 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
990 if (contactUri == null) {
991 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
992 return;
993 }
994
995 final ContentValues values = new ContentValues(1);
996 values.put(Contacts.SEND_TO_VOICEMAIL, value);
997 getContentResolver().update(contactUri, values, null, null);
998 }
999
1000 /**
1001 * Creates an intent that can be sent to this service to save the contact's ringtone.
1002 */
1003 public static Intent createSetRingtone(Context context, Uri contactUri,
1004 String value) {
1005 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1006 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1007 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1008 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1009
1010 return serviceIntent;
1011 }
1012
1013 private void setRingtone(Intent intent) {
1014 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1015 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1016 if (contactUri == null) {
1017 Log.e(TAG, "Invalid arguments for setRingtone");
1018 return;
1019 }
1020 ContentValues values = new ContentValues(1);
1021 values.put(Contacts.CUSTOM_RINGTONE, value);
1022 getContentResolver().update(contactUri, values, null, null);
1023 }
1024
1025 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001026 * Creates an intent that sets the selected data item as super primary (default)
1027 */
1028 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1029 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1030 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1031 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1032 return serviceIntent;
1033 }
1034
1035 private void setSuperPrimary(Intent intent) {
1036 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1037 if (dataId == -1) {
1038 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1039 return;
1040 }
1041
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001042 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001043 }
1044
1045 /**
1046 * Creates an intent that clears the primary flag of all data items that belong to the same
1047 * raw_contact as the given data item. Will only clear, if the data item was primary before
1048 * this call
1049 */
1050 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1051 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1052 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1053 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1054 return serviceIntent;
1055 }
1056
1057 private void clearPrimary(Intent intent) {
1058 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1059 if (dataId == -1) {
1060 Log.e(TAG, "Invalid arguments for clearPrimary request");
1061 return;
1062 }
1063
1064 // Update the primary values in the data record.
1065 ContentValues values = new ContentValues(1);
1066 values.put(Data.IS_SUPER_PRIMARY, 0);
1067 values.put(Data.IS_PRIMARY, 0);
1068
1069 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1070 values, null, null);
1071 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001072
1073 /**
1074 * Creates an intent that can be sent to this service to delete a contact.
1075 */
1076 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1077 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1078 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1079 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1080 return serviceIntent;
1081 }
1082
Brian Attwelld2962a32015-03-02 14:48:50 -08001083 /**
1084 * Creates an intent that can be sent to this service to delete multiple contacts.
1085 */
1086 public static Intent createDeleteMultipleContactsIntent(Context context,
1087 long[] contactIds) {
1088 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1089 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1090 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1091 return serviceIntent;
1092 }
1093
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001094 private void deleteContact(Intent intent) {
1095 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1096 if (contactUri == null) {
1097 Log.e(TAG, "Invalid arguments for deleteContact request");
1098 return;
1099 }
1100
1101 getContentResolver().delete(contactUri, null, null);
1102 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001103
Brian Attwelld2962a32015-03-02 14:48:50 -08001104 private void deleteMultipleContacts(Intent intent) {
1105 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1106 if (contactIds == null) {
1107 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1108 return;
1109 }
1110 for (long contactId : contactIds) {
1111 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1112 getContentResolver().delete(contactUri, null, null);
1113 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001114 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1115 .contacts_deleted_toast, contactIds.length);
1116 mMainHandler.post(new Runnable() {
1117 @Override
1118 public void run() {
1119 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1120 .show();
1121 }
1122 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001123 }
1124
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001125 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001126 * Creates an intent that can be sent to this service to split a contact into it's constituent
1127 * pieces.
1128 */
1129 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1130 ResultReceiver receiver) {
1131 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1132 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1133 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1134 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1135 return serviceIntent;
1136 }
1137
1138 private void splitContact(Intent intent) {
1139 final long rawContactIds[][] = (long[][]) intent
1140 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
1141 if (rawContactIds == null) {
1142 Log.e(TAG, "Invalid argument for splitContact request");
1143 return;
1144 }
1145 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1146 final ContentResolver resolver = getContentResolver();
1147 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
1148 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
1149 for (int i = 0; i < rawContactIds.length; i++) {
1150 for (int j = 0; j < rawContactIds.length; j++) {
1151 if (i != j) {
1152 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1153 if (receiver != null) {
1154 receiver.send(CP2_ERROR, new Bundle());
1155 return;
1156 }
1157 }
1158 }
1159 }
1160 }
1161 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1162 if (receiver != null) {
1163 receiver.send(CP2_ERROR, new Bundle());
1164 }
1165 return;
1166 }
1167 if (receiver != null) {
1168 receiver.send(CONTACTS_SPLIT, new Bundle());
1169 } else {
1170 showToast(R.string.contactUnlinkedToast);
1171 }
1172 }
1173
1174 /**
1175 * Adds insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
1176 * and {@param rawContactIds2} to {@param operations}.
1177 * @return false if an error occurred, true otherwise.
1178 */
1179 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1180 long[] rawContactIds1, long[] rawContactIds2) {
1181 if (rawContactIds1 == null || rawContactIds2 == null) {
1182 Log.e(TAG, "Invalid arguments for splitContact request");
1183 return false;
1184 }
1185 // For each pair of raw contacts, insert an aggregation exception
1186 final ContentResolver resolver = getContentResolver();
1187 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1188 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1189 for (int i = 0; i < rawContactIds1.length; i++) {
1190 for (int j = 0; j < rawContactIds2.length; j++) {
1191 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1192 // Before we get to 500 we need to flush the operations list
1193 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1194 if (!applyOperations(resolver, operations)) {
1195 return false;
1196 }
1197 operations.clear();
1198 }
1199 }
1200 }
1201 return true;
1202 }
1203
1204 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001205 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001206 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001207 */
1208 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001209 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001210 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1211 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1212 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1213 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001214
1215 // Callback intent will be invoked by the service once the contacts are joined.
1216 Intent callbackIntent = new Intent(context, callbackActivity);
1217 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001218 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1219
1220 return serviceIntent;
1221 }
1222
Brian Attwelld3946ca2015-03-03 11:13:49 -08001223 /**
1224 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1225 * No special attention is paid to where the resulting contact's name is taken from.
1226 */
Gary Mai7efa9942016-05-12 11:26:49 -07001227 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1228 ResultReceiver receiver) {
1229 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001230 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1231 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001232 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001233 return serviceIntent;
1234 }
1235
Gary Mai7efa9942016-05-12 11:26:49 -07001236 /**
1237 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1238 * No special attention is paid to where the resulting contact's name is taken from.
1239 */
1240 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1241 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1242 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001243
1244 private interface JoinContactQuery {
1245 String[] PROJECTION = {
1246 RawContacts._ID,
1247 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001248 RawContacts.DISPLAY_NAME_SOURCE,
1249 };
1250
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001251 int _ID = 0;
1252 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001253 int DISPLAY_NAME_SOURCE = 2;
1254 }
1255
1256 private interface ContactEntityQuery {
1257 String[] PROJECTION = {
1258 Contacts.Entity.DATA_ID,
1259 Contacts.Entity.CONTACT_ID,
1260 Contacts.Entity.IS_SUPER_PRIMARY,
1261 };
1262 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1263 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1264 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1265 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1266
1267 int DATA_ID = 0;
1268 int CONTACT_ID = 1;
1269 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001270 }
1271
Brian Attwelld3946ca2015-03-03 11:13:49 -08001272 private void joinSeveralContacts(Intent intent) {
1273 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Gary Mai7efa9942016-05-12 11:26:49 -07001274 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001275
Brian Attwelld3946ca2015-03-03 11:13:49 -08001276 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001277 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1278 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001279 if (rawContactIds == null) {
1280 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001281 return;
1282 }
1283
Brian Attwelld3946ca2015-03-03 11:13:49 -08001284 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001285 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001286 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1287 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1288 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001289 for (int i = 0; i < rawContactIds.length; i++) {
1290 for (int j = 0; j < rawContactIds.length; j++) {
1291 if (i != j) {
1292 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1293 }
Walter Jang0653de32015-07-24 12:12:40 -07001294 // Before we get to 500 we need to flush the operations list
1295 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001296 if (!applyOperations(resolver, operations)) {
1297 if (receiver != null) {
1298 receiver.send(CP2_ERROR, new Bundle());
1299 }
Walter Jang0653de32015-07-24 12:12:40 -07001300 return;
1301 }
1302 operations.clear();
1303 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001304 }
1305 }
Gary Mai7efa9942016-05-12 11:26:49 -07001306 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1307 if (receiver != null) {
1308 receiver.send(CP2_ERROR, new Bundle());
1309 }
Walter Jang0653de32015-07-24 12:12:40 -07001310 return;
1311 }
Gary Mai7efa9942016-05-12 11:26:49 -07001312 if (receiver != null) {
1313 final Bundle result = new Bundle();
1314 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1315 receiver.send(CONTACTS_LINKED, result);
1316 } else {
1317 showToast(R.string.contactsJoinedMessage);
1318 }
Walter Jang0653de32015-07-24 12:12:40 -07001319 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001320
Walter Jang0653de32015-07-24 12:12:40 -07001321 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001322 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001323 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001324 try {
1325 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001326 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001327 } catch (RemoteException | OperationApplicationException e) {
1328 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1329 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001330 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001331 }
1332 }
1333
Brian Attwelld3946ca2015-03-03 11:13:49 -08001334 private void joinContacts(Intent intent) {
1335 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1336 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001337
1338 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001339 // in the join UIs.
1340 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1341 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001342 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001343 return;
1344 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001345
Brian Attwell548f5c62015-01-27 17:46:46 -08001346 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001347
1348 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001349 for (int i = 0; i < rawContactIds.length; i++) {
1350 for (int j = 0; j < rawContactIds.length; j++) {
1351 if (i != j) {
1352 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1353 }
1354 }
1355 }
1356
Brian Attwelld3946ca2015-03-03 11:13:49 -08001357 final ContentResolver resolver = getContentResolver();
1358
Brian Attwell548f5c62015-01-27 17:46:46 -08001359 // Use the name for contactId1 as the name for the newly aggregated contact.
1360 final Uri contactId1Uri = ContentUris.withAppendedId(
1361 Contacts.CONTENT_URI, contactId1);
1362 final Uri entityUri = Uri.withAppendedPath(
1363 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1364 Cursor c = resolver.query(entityUri,
1365 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1366 if (c == null) {
1367 Log.e(TAG, "Unable to open Contacts DB cursor");
1368 showToast(R.string.contactSavedErrorToast);
1369 return;
1370 }
1371 long dataIdToAddSuperPrimary = -1;
1372 try {
1373 if (c.moveToFirst()) {
1374 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1375 }
1376 } finally {
1377 c.close();
1378 }
1379
1380 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1381 // display name does not change as a result of the join.
1382 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001383 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001384 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1385 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1386 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001387 operations.add(builder.build());
1388 }
1389
1390 boolean success = false;
1391 // Apply all aggregation exceptions as one batch
1392 try {
1393 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001394 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001395 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001396 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001397 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001398 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001399 }
1400
1401 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1402 if (success) {
1403 Uri uri = RawContacts.getContactLookupUri(resolver,
1404 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1405 callbackIntent.setData(uri);
1406 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001407 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001408 }
1409
Gary Mai7efa9942016-05-12 11:26:49 -07001410 /**
1411 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1412 * array of the return value holds an array of raw contact ids for one contactId.
1413 * @param contactIds
1414 * @return
1415 */
1416 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1417 final long[][] rawContactIds = new long[contactIds.length][];
1418 for (int i = 0; i < contactIds.length; i++) {
1419 rawContactIds[i] = getRawContactIds(contactIds[i]);
1420 }
1421 return rawContactIds;
1422 }
1423
1424 /**
1425 * Gets the raw contact ids associated with {@param contactId}.
1426 * @param contactId
1427 * @return Array of raw contact ids.
1428 */
1429 private long[] getRawContactIds(long contactId) {
1430 final ContentResolver resolver = getContentResolver();
1431 long rawContactIds[];
1432
1433 final StringBuilder queryBuilder = new StringBuilder();
1434 queryBuilder.append(RawContacts.CONTACT_ID)
1435 .append("=")
1436 .append(String.valueOf(contactId));
1437
1438 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1439 JoinContactQuery.PROJECTION,
1440 queryBuilder.toString(),
1441 null, null);
1442 if (c == null) {
1443 Log.e(TAG, "Unable to open Contacts DB cursor");
1444 return null;
1445 }
1446 try {
1447 rawContactIds = new long[c.getCount()];
1448 for (int i = 0; i < rawContactIds.length; i++) {
1449 c.moveToPosition(i);
1450 final long rawContactId = c.getLong(JoinContactQuery._ID);
1451 rawContactIds[i] = rawContactId;
1452 }
1453 } finally {
1454 c.close();
1455 }
1456 return rawContactIds;
1457 }
1458
Brian Attwelld3946ca2015-03-03 11:13:49 -08001459 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1460 if (contactIds == null) {
1461 return null;
1462 }
1463
Brian Attwell548f5c62015-01-27 17:46:46 -08001464 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001465
1466 final StringBuilder queryBuilder = new StringBuilder();
1467 final String stringContactIds[] = new String[contactIds.length];
1468 for (int i = 0; i < contactIds.length; i++) {
1469 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1470 stringContactIds[i] = String.valueOf(contactIds[i]);
1471 if (contactIds[i] == -1) {
1472 return null;
1473 }
1474 if (i == contactIds.length -1) {
1475 break;
1476 }
1477 queryBuilder.append(" OR ");
1478 }
1479
Brian Attwell548f5c62015-01-27 17:46:46 -08001480 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1481 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001482 queryBuilder.toString(),
1483 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001484 if (c == null) {
1485 Log.e(TAG, "Unable to open Contacts DB cursor");
1486 showToast(R.string.contactSavedErrorToast);
1487 return null;
1488 }
Gary Mai7efa9942016-05-12 11:26:49 -07001489 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001490 try {
1491 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001492 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001493 return null;
1494 }
1495 rawContactIds = new long[c.getCount()];
1496 for (int i = 0; i < rawContactIds.length; i++) {
1497 c.moveToPosition(i);
1498 long rawContactId = c.getLong(JoinContactQuery._ID);
1499 rawContactIds[i] = rawContactId;
1500 }
1501 } finally {
1502 c.close();
1503 }
1504 return rawContactIds;
1505 }
1506
Brian Attwelld3946ca2015-03-03 11:13:49 -08001507 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1508 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1509 }
1510
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001511 /**
1512 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1513 */
1514 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1515 long rawContactId1, long rawContactId2) {
1516 Builder builder =
1517 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1518 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1519 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1520 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1521 operations.add(builder.build());
1522 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001523
1524 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001525 * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation.
1526 */
1527 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1528 long rawContactId1, long rawContactId2) {
1529 final Builder builder =
1530 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1531 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
1532 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1533 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1534 operations.add(builder.build());
1535 }
1536
1537 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001538 * Shows a toast on the UI thread.
1539 */
1540 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001541 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001542
1543 @Override
1544 public void run() {
1545 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1546 }
1547 });
1548 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001549
1550 private void deliverCallback(final Intent callbackIntent) {
1551 mMainHandler.post(new Runnable() {
1552
1553 @Override
1554 public void run() {
1555 deliverCallbackOnUiThread(callbackIntent);
1556 }
1557 });
1558 }
1559
1560 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1561 // TODO: this assumes that if there are multiple instances of the same
1562 // activity registered, the last one registered is the one waiting for
1563 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001564 for (Listener listener : sListeners) {
1565 if (callbackIntent.getComponent().equals(
1566 ((Activity) listener).getIntent().getComponent())) {
1567 listener.onServiceCompleted(callbackIntent);
1568 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001569 }
1570 }
1571 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001572}