blob: 517305a719824ebe3a90283d0c0938aebeed130e [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;
Wenyi Wange5bac732016-02-03 11:55:21 -080059import com.android.contacts.common.model.account.AccountType;
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;
Walter Jang0ae4a932016-02-19 15:01:32 -080063import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor.SaveMode;
Yorke Lee637a38e2013-09-14 08:36:33 -070064import com.android.contacts.util.ContactPhotoUtils;
65
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070066import com.google.common.collect.Lists;
67import com.google.common.collect.Sets;
68
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080069import java.util.ArrayList;
70import java.util.HashSet;
Wenyi Wange5bac732016-02-03 11:55:21 -080071import java.util.Iterator;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080072import java.util.List;
73import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080075/**
76 * A service responsible for saving changes to the content provider.
77 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070078public class ContactSaveService extends IntentService {
79 private static final String TAG = "ContactSaveService";
80
Katherine Kuana007e442011-07-07 09:25:34 -070081 /** Set to true in order to view logs on content provider operations */
82 private static final boolean DEBUG = false;
83
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070084 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
85
86 public static final String EXTRA_ACCOUNT_NAME = "accountName";
87 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070088 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070089 public static final String EXTRA_CONTENT_VALUES = "contentValues";
90 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
91
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
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800119 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800120 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800121 public static final String EXTRA_CONTACT_ID1 = "contactId1";
122 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800123
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700124 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
125 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
126
127 public static final String ACTION_SET_RINGTONE = "setRingtone";
128 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
129
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700130 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
131 Data.MIMETYPE,
132 Data.IS_PRIMARY,
133 Data.DATA1,
134 Data.DATA2,
135 Data.DATA3,
136 Data.DATA4,
137 Data.DATA5,
138 Data.DATA6,
139 Data.DATA7,
140 Data.DATA8,
141 Data.DATA9,
142 Data.DATA10,
143 Data.DATA11,
144 Data.DATA12,
145 Data.DATA13,
146 Data.DATA14,
147 Data.DATA15
148 );
149
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800150 private static final int PERSIST_TRIES = 3;
151
Walter Jang0653de32015-07-24 12:12:40 -0700152 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
153
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800154 public interface Listener {
155 public void onServiceCompleted(Intent callbackIntent);
156 }
157
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100158 private static final CopyOnWriteArrayList<Listener> sListeners =
159 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800160
161 private Handler mMainHandler;
162
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700163 public ContactSaveService() {
164 super(TAG);
165 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800166 mMainHandler = new Handler(Looper.getMainLooper());
167 }
168
169 public static void registerListener(Listener listener) {
170 if (!(listener instanceof Activity)) {
171 throw new ClassCastException("Only activities can be registered to"
172 + " receive callback from " + ContactSaveService.class.getName());
173 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100174 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800175 }
176
177 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100178 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700179 }
180
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800181 /**
182 * Returns true if the ContactSaveService was started successfully and false if an exception
183 * was thrown and a Toast error message was displayed.
184 */
185 public static boolean startService(Context context, Intent intent, int saveMode) {
186 try {
187 context.startService(intent);
188 } catch (Exception exception) {
189 final int resId;
190 switch (saveMode) {
191 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
192 resId = R.string.contactUnlinkErrorToast;
193 break;
194 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
195 resId = R.string.contactJoinErrorToast;
196 break;
197 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
198 resId = R.string.contactSavedErrorToast;
199 break;
200 default:
201 resId = R.string.contactGenericErrorToast;
202 }
203 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
204 return false;
205 }
206 return true;
207 }
208
209 /**
210 * Utility method that starts service and handles exception.
211 */
212 public static void startService(Context context, Intent intent) {
213 try {
214 context.startService(intent);
215 } catch (Exception exception) {
216 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
217 }
218 }
219
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700220 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800221 public Object getSystemService(String name) {
222 Object service = super.getSystemService(name);
223 if (service != null) {
224 return service;
225 }
226
227 return getApplicationContext().getSystemService(name);
228 }
229
230 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700231 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800232 if (intent == null) {
233 Log.d(TAG, "onHandleIntent: could not handle null intent");
234 return;
235 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700236 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
237 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
238 // TODO: add more specific error string such as "Turn on Contacts
239 // permission to update your contacts"
240 showToast(R.string.contactSavedErrorToast);
241 return;
242 }
243
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700244 // Call an appropriate method. If we're sure it affects how incoming phone calls are
245 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700246 String action = intent.getAction();
247 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
248 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800249 } else if (ACTION_SAVE_CONTACT.equals(action)) {
250 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800251 } else if (ACTION_CREATE_GROUP.equals(action)) {
252 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800253 } else if (ACTION_RENAME_GROUP.equals(action)) {
254 renameGroup(intent);
255 } else if (ACTION_DELETE_GROUP.equals(action)) {
256 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700257 } else if (ACTION_UPDATE_GROUP.equals(action)) {
258 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800259 } else if (ACTION_SET_STARRED.equals(action)) {
260 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800261 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
262 setSuperPrimary(intent);
263 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
264 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800265 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
266 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800267 } else if (ACTION_DELETE_CONTACT.equals(action)) {
268 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800269 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
270 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800271 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
272 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700273 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
274 setSendToVoicemail(intent);
275 } else if (ACTION_SET_RINGTONE.equals(action)) {
276 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700277 }
278 }
279
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800280 /**
281 * Creates an intent that can be sent to this service to create a new raw contact
282 * using data presented as a set of ContentValues.
283 */
284 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700285 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700286 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800287 Intent serviceIntent = new Intent(
288 context, ContactSaveService.class);
289 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
290 if (account != null) {
291 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
292 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700293 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800294 }
295 serviceIntent.putParcelableArrayListExtra(
296 ContactSaveService.EXTRA_CONTENT_VALUES, values);
297
298 // Callback intent will be invoked by the service once the new contact is
299 // created. The service will put the URI of the new contact as "data" on
300 // the callback intent.
301 Intent callbackIntent = new Intent(context, callbackActivity);
302 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800303 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
304 return serviceIntent;
305 }
306
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700307 private void createRawContact(Intent intent) {
308 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
309 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700310 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700311 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
312 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
313
314 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
315 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
316 .withValue(RawContacts.ACCOUNT_NAME, accountName)
317 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700318 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700319 .build());
320
321 int size = valueList.size();
322 for (int i = 0; i < size; i++) {
323 ContentValues values = valueList.get(i);
324 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
325 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
326 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
327 .withValues(values)
328 .build());
329 }
330
331 ContentResolver resolver = getContentResolver();
332 ContentProviderResult[] results;
333 try {
334 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
335 } catch (Exception e) {
336 throw new RuntimeException("Failed to store new contact", e);
337 }
338
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700339 Uri rawContactUri = results[0].uri;
340 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
341
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800342 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700343 }
344
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700345 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800346 * Creates an intent that can be sent to this service to create a new raw contact
347 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800348 * This variant is more convenient to use when there is only one photo that can
349 * possibly be updated, as in the Contact Details screen.
350 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
351 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352 */
Maurice Chu851222a2012-06-21 11:43:08 -0700353 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700354 String saveModeExtraKey, int saveMode, boolean isProfile,
355 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700356 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800357 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700358 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800359 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700360 callbackActivity, callbackAction, bundle,
361 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800362 }
363
364 /**
365 * Creates an intent that can be sent to this service to create a new raw contact
366 * using data presented as a set of ContentValues.
367 * This variant is used when multiple contacts' photos may be updated, as in the
368 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700369 *
Josh Garguse692e012012-01-18 14:53:11 -0800370 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700371 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
372 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800373 */
Maurice Chu851222a2012-06-21 11:43:08 -0700374 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700375 String saveModeExtraKey, int saveMode, boolean isProfile,
376 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700377 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jang0ae4a932016-02-19 15:01:32 -0800378
Wenyi Wange5bac732016-02-03 11:55:21 -0800379 // Don't pass read-only RawContactDeltas in RawContactDeltaList to contact save service,
380 // because 1. read-only RawContactDeltas are not writable anyway; 2. read-only
381 // RawContactDeltas may be problematic, see b/23896510.
Walter Jang0ae4a932016-02-19 15:01:32 -0800382 // Except when we must create aggregation exceptions between the raw contacts
383 if (!(saveMode == SaveMode.JOIN || saveMode == SaveMode.SPLIT)) {
384 removeReadOnlyContacts(context, state);
385 }
Wenyi Wange5bac732016-02-03 11:55:21 -0800386
387 Intent serviceIntent = new Intent(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
Wenyi Wange5bac732016-02-03 11:55:21 -0800412 private static void removeReadOnlyContacts(Context context, RawContactDeltaList state) {
413 if (Log.isLoggable(TAG, Log.VERBOSE)) {
414 Log.v(TAG, "Before trimming: " + state.size());
415 }
416 int countReadOnly = 0;
417 final Iterator<RawContactDelta> iterator = state.iterator();
418 while (iterator.hasNext()) {
419 final RawContactDelta rawContactDelta = iterator.next();
420 final AccountType accountType = rawContactDelta.getRawContactAccountType(context);
421 if (accountType != null && !accountType.areContactsWritable()) {
422 countReadOnly++;
423 iterator.remove();
424 }
425 }
426 if (Log.isLoggable(TAG, Log.VERBOSE)) {
427 Log.v(TAG, "# of read-only removed: " + countReadOnly);
428 Log.v(TAG, "After trimming: " + state.size());
429 }
430 }
431
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800432 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700433 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700434 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800435 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800436
Jay Shrauner08099782015-03-25 14:17:11 -0700437 if (state == null) {
438 Log.e(TAG, "Invalid arguments for saveContact request");
439 return;
440 }
441
benny.lin3a4e7a22014-01-08 10:58:08 +0800442 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800443 // Trim any empty fields, and RawContacts, before persisting
444 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700445 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800446
447 Uri lookupUri = null;
448
449 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700450
Josh Garguse692e012012-01-18 14:53:11 -0800451 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800452
Josh Gargusef15c8e2012-01-30 16:42:02 -0800453 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
454 long insertedRawContactId = -1;
455
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800456 // Attempt to persist changes
457 int tries = 0;
458 while (tries++ < PERSIST_TRIES) {
459 try {
460 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800461 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
462
463 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
464
465 for (CPOWrapper cpoWrapper : diffWrapper) {
466 diff.add(cpoWrapper.getOperation());
467 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700468
Katherine Kuana007e442011-07-07 09:25:34 -0700469 if (DEBUG) {
470 Log.v(TAG, "Content Provider Operations:");
471 for (ContentProviderOperation operation : diff) {
472 Log.v(TAG, operation.toString());
473 }
474 }
475
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700476 int numberProcessed = 0;
477 boolean batchFailed = false;
478 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
479 while (numberProcessed < diff.size()) {
480 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
481 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700482 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700483 batchFailed = true;
484 break;
485 } else {
486 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700487 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800488 }
489
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700490 if (batchFailed) {
491 // Retry save
492 continue;
493 }
494
Wenyi Wang67addcc2015-11-23 10:07:48 -0800495 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800496 if (rawContactId == -1) {
497 throw new IllegalStateException("Could not determine RawContact ID after save");
498 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800499 // We don't have to check to see if the value is still -1. If we reach here,
500 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800501 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700502 if (isProfile) {
503 // Since the profile supports local raw contacts, which may have been completely
504 // removed if all information was removed, we need to do a special query to
505 // get the lookup URI for the profile contact (if it still exists).
506 Cursor c = resolver.query(Profile.CONTENT_URI,
507 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
508 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800509 if (c == null) {
510 continue;
511 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700512 try {
Erik162b7e32011-09-20 15:23:55 -0700513 if (c.moveToFirst()) {
514 final long contactId = c.getLong(0);
515 final String lookupKey = c.getString(1);
516 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
517 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700518 } finally {
519 c.close();
520 }
521 } else {
522 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
523 rawContactId);
524 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
525 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800526 if (lookupUri != null) {
527 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
528 }
Josh Garguse692e012012-01-18 14:53:11 -0800529
530 // We can change this back to false later, if we fail to save the contact photo.
531 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800532 break;
533
534 } catch (RemoteException e) {
535 // Something went wrong, bail without success
536 Log.e(TAG, "Problem persisting user edits", e);
537 break;
538
Jay Shrauner57fca182014-01-17 14:20:50 -0800539 } catch (IllegalArgumentException e) {
540 // This is thrown by applyBatch on malformed requests
541 Log.e(TAG, "Problem persisting user edits", e);
542 showToast(R.string.contactSavedErrorToast);
543 break;
544
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800545 } catch (OperationApplicationException e) {
546 // Version consistency failed, re-parent change and try again
547 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
548 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
549 boolean first = true;
550 final int count = state.size();
551 for (int i = 0; i < count; i++) {
552 Long rawContactId = state.getRawContactId(i);
553 if (rawContactId != null && rawContactId != -1) {
554 if (!first) {
555 sb.append(',');
556 }
557 sb.append(rawContactId);
558 first = false;
559 }
560 }
561 sb.append(")");
562
563 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800564 throw new IllegalStateException(
565 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800566 }
567
Maurice Chu851222a2012-06-21 11:43:08 -0700568 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700569 isProfile
570 ? RawContactsEntity.PROFILE_CONTENT_URI
571 : RawContactsEntity.CONTENT_URI,
572 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700573 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700574
575 // Update the new state to use profile URIs if appropriate.
576 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700577 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700578 delta.setProfileQueryUri();
579 }
580 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800581 }
582 }
583
Josh Garguse692e012012-01-18 14:53:11 -0800584 // Now save any updated photos. We do this at the end to ensure that
585 // the ContactProvider already knows about newly-created contacts.
586 if (updatedPhotos != null) {
587 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700588 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800589 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800590
591 // If the raw-contact ID is negative, we are saving a new raw-contact;
592 // replace the bogus ID with the new one that we actually saved the contact at.
593 if (rawContactId < 0) {
594 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800595 }
596
Jay Shrauner511561d2015-04-02 10:35:33 -0700597 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700598 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700599 succeeded = false;
600 }
Josh Garguse692e012012-01-18 14:53:11 -0800601 }
602 }
603
Josh Garguse5d3f892012-04-11 11:56:15 -0700604 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
605 if (callbackIntent != null) {
606 if (succeeded) {
607 // Mark the intent to indicate that the save was successful (even if the lookup URI
608 // is now null). For local contacts or the local profile, it's possible that the
609 // save triggered removal of the contact, so no lookup URI would exist..
610 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
611 }
612 callbackIntent.setData(lookupUri);
613 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800614 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800615 }
616
Josh Garguse692e012012-01-18 14:53:11 -0800617 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700618 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
619 * subsets, adds the returned array to "results".
620 *
621 * @return the size of the array, if not null; -1 when the array is null.
622 */
623 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
624 ContentProviderResult[] results, ContentResolver resolver)
625 throws RemoteException, OperationApplicationException {
626 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
627 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
628 subset.addAll(diff.subList(offset, offset + subsetCount));
629 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
630 .AUTHORITY, subset);
631 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
632 return -1;
633 }
634 for (ContentProviderResult c : subsetResult) {
635 results[offset++] = c;
636 }
637 return subsetResult.length;
638 }
639
640 /**
Josh Garguse692e012012-01-18 14:53:11 -0800641 * Save updated photo for the specified raw-contact.
642 * @return true for success, false for failure
643 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800644 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800645 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800646 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
647 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
648
benny.lin3a4e7a22014-01-08 10:58:08 +0800649 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800650 }
651
Josh Gargusef15c8e2012-01-30 16:42:02 -0800652 /**
653 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
654 */
Maurice Chu851222a2012-06-21 11:43:08 -0700655 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800656 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800657 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800658 long existingRawContactId = state.findRawContactId();
659 if (existingRawContactId != -1) {
660 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800661 }
662
Wenyi Wang67addcc2015-11-23 10:07:48 -0800663 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800664 }
665
666 /**
667 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
668 */
669 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800670 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800671 if (results == null) {
672 return -1;
673 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800674 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800675 final int numResults = results.length;
676 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800677 final CPOWrapper cpoWrapper = diffWrapper.get(i);
678 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
679 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
680 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800681 return ContentUris.parseId(results[i].uri);
682 }
683 }
684 return -1;
685 }
686
687 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700688 * Creates an intent that can be sent to this service to create a new group as
689 * well as add new members at the same time.
690 *
691 * @param context of the application
692 * @param account in which the group should be created
693 * @param label is the name of the group (cannot be null)
694 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
695 * should be added to the group
696 * @param callbackActivity is the activity to send the callback intent to
697 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700698 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700699 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700700 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700701 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800702 Intent serviceIntent = new Intent(context, ContactSaveService.class);
703 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
705 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700706 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800707 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700708 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700709
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800710 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700711 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800712 Intent callbackIntent = new Intent(context, callbackActivity);
713 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700714 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800715
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700716 return serviceIntent;
717 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800718
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800719 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700720 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
721 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
722 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
723 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700724 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800725
726 ContentValues values = new ContentValues();
727 values.put(Groups.ACCOUNT_TYPE, accountType);
728 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700729 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800730 values.put(Groups.TITLE, label);
731
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 final ContentResolver resolver = getContentResolver();
733
734 // Create the new group
735 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
736
737 // If there's no URI, then the insertion failed. Abort early because group members can't be
738 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800739 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700740 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800741 return;
742 }
743
Katherine Kuan717e3432011-07-13 17:03:24 -0700744 // Add new group members
745 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
746
747 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
748 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800749 values.clear();
750 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
751 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
752
753 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700754 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700755 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800756 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800757 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800758 }
759
760 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800761 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800762 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700763 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700764 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800765 Intent serviceIntent = new Intent(context, ContactSaveService.class);
766 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
767 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
768 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700769
770 // Callback intent will be invoked by the service once the group is renamed.
771 Intent callbackIntent = new Intent(context, callbackActivity);
772 callbackIntent.setAction(callbackAction);
773 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
774
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800775 return serviceIntent;
776 }
777
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800778 private void renameGroup(Intent intent) {
779 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
780 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
781
782 if (groupId == -1) {
783 Log.e(TAG, "Invalid arguments for renameGroup request");
784 return;
785 }
786
787 ContentValues values = new ContentValues();
788 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700789 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
790 getContentResolver().update(groupUri, values, null, null);
791
792 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
793 callbackIntent.setData(groupUri);
794 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800795 }
796
797 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800798 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800799 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800800 public static Intent createGroupDeletionIntent(Context context, long groupId) {
801 Intent serviceIntent = new Intent(context, ContactSaveService.class);
802 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800803 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800804 return serviceIntent;
805 }
806
807 private void deleteGroup(Intent intent) {
808 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
809 if (groupId == -1) {
810 Log.e(TAG, "Invalid arguments for deleteGroup request");
811 return;
812 }
813
814 getContentResolver().delete(
815 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
816 }
817
818 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700819 * Creates an intent that can be sent to this service to rename a group as
820 * well as add and remove members from the group.
821 *
822 * @param context of the application
823 * @param groupId of the group that should be modified
824 * @param newLabel is the updated name of the group (can be null if the name
825 * should not be updated)
826 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
827 * should be added to the group
828 * @param rawContactsToRemove is an array of raw contact IDs for contacts
829 * that should be removed from the group
830 * @param callbackActivity is the activity to send the callback intent to
831 * @param callbackAction is the intent action for the callback intent
832 */
833 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
834 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700835 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700836 Intent serviceIntent = new Intent(context, ContactSaveService.class);
837 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
838 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
839 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
840 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
841 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
842 rawContactsToRemove);
843
844 // Callback intent will be invoked by the service once the group is updated
845 Intent callbackIntent = new Intent(context, callbackActivity);
846 callbackIntent.setAction(callbackAction);
847 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
848
849 return serviceIntent;
850 }
851
852 private void updateGroup(Intent intent) {
853 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
854 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
855 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
856 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
857
858 if (groupId == -1) {
859 Log.e(TAG, "Invalid arguments for updateGroup request");
860 return;
861 }
862
863 final ContentResolver resolver = getContentResolver();
864 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
865
866 // Update group name if necessary
867 if (label != null) {
868 ContentValues values = new ContentValues();
869 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700870 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700871 }
872
Katherine Kuan717e3432011-07-13 17:03:24 -0700873 // Add and remove members if necessary
874 addMembersToGroup(resolver, rawContactsToAdd, groupId);
875 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
876
877 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
878 callbackIntent.setData(groupUri);
879 deliverCallback(callbackIntent);
880 }
881
Daniel Lehmann18958a22012-02-28 17:45:25 -0800882 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700883 long groupId) {
884 if (rawContactsToAdd == null) {
885 return;
886 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700887 for (long rawContactId : rawContactsToAdd) {
888 try {
889 final ArrayList<ContentProviderOperation> rawContactOperations =
890 new ArrayList<ContentProviderOperation>();
891
892 // Build an assert operation to ensure the contact is not already in the group
893 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
894 .newAssertQuery(Data.CONTENT_URI);
895 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
896 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
897 new String[] { String.valueOf(rawContactId),
898 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
899 assertBuilder.withExpectedCount(0);
900 rawContactOperations.add(assertBuilder.build());
901
902 // Build an insert operation to add the contact to the group
903 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
904 .newInsert(Data.CONTENT_URI);
905 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
906 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
907 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
908 rawContactOperations.add(insertBuilder.build());
909
910 if (DEBUG) {
911 for (ContentProviderOperation operation : rawContactOperations) {
912 Log.v(TAG, operation.toString());
913 }
914 }
915
916 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700917 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800918 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700919 }
920 } catch (RemoteException e) {
921 // Something went wrong, bail without success
922 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
923 String.valueOf(rawContactId), e);
924 } catch (OperationApplicationException e) {
925 // The assert could have failed because the contact is already in the group,
926 // just continue to the next contact
927 Log.w(TAG, "Assert failed in adding raw contact ID " +
928 String.valueOf(rawContactId) + ". Already exists in group " +
929 String.valueOf(groupId), e);
930 }
931 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700932 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700933
Daniel Lehmann18958a22012-02-28 17:45:25 -0800934 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700935 long groupId) {
936 if (rawContactsToRemove == null) {
937 return;
938 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700939 for (long rawContactId : rawContactsToRemove) {
940 // Apply the delete operation on the data row for the given raw contact's
941 // membership in the given group. If no contact matches the provided selection, then
942 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800943 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700944 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
945 new String[] { String.valueOf(rawContactId),
946 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
947 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700948 }
949
950 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800951 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800952 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800953 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
954 Intent serviceIntent = new Intent(context, ContactSaveService.class);
955 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
956 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
957 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
958
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800959 return serviceIntent;
960 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800961
962 private void setStarred(Intent intent) {
963 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
964 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
965 if (contactUri == null) {
966 Log.e(TAG, "Invalid arguments for setStarred request");
967 return;
968 }
969
970 final ContentValues values = new ContentValues(1);
971 values.put(Contacts.STARRED, value);
972 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700973
974 // Undemote the contact if necessary
975 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
976 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800977 if (c == null) {
978 return;
979 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700980 try {
981 if (c.moveToFirst()) {
982 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700983
984 // Don't bother undemoting if this contact is the user's profile.
985 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800986 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700987 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700988 }
989 } finally {
990 c.close();
991 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800992 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800993
994 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700995 * Creates an intent that can be sent to this service to set the redirect to voicemail.
996 */
997 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
998 boolean value) {
999 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1000 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1001 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1002 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1003
1004 return serviceIntent;
1005 }
1006
1007 private void setSendToVoicemail(Intent intent) {
1008 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1009 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1010 if (contactUri == null) {
1011 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1012 return;
1013 }
1014
1015 final ContentValues values = new ContentValues(1);
1016 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1017 getContentResolver().update(contactUri, values, null, null);
1018 }
1019
1020 /**
1021 * Creates an intent that can be sent to this service to save the contact's ringtone.
1022 */
1023 public static Intent createSetRingtone(Context context, Uri contactUri,
1024 String value) {
1025 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1026 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1027 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1028 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1029
1030 return serviceIntent;
1031 }
1032
1033 private void setRingtone(Intent intent) {
1034 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1035 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1036 if (contactUri == null) {
1037 Log.e(TAG, "Invalid arguments for setRingtone");
1038 return;
1039 }
1040 ContentValues values = new ContentValues(1);
1041 values.put(Contacts.CUSTOM_RINGTONE, value);
1042 getContentResolver().update(contactUri, values, null, null);
1043 }
1044
1045 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001046 * Creates an intent that sets the selected data item as super primary (default)
1047 */
1048 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1049 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1050 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1051 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1052 return serviceIntent;
1053 }
1054
1055 private void setSuperPrimary(Intent intent) {
1056 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1057 if (dataId == -1) {
1058 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1059 return;
1060 }
1061
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001062 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001063 }
1064
1065 /**
1066 * Creates an intent that clears the primary flag of all data items that belong to the same
1067 * raw_contact as the given data item. Will only clear, if the data item was primary before
1068 * this call
1069 */
1070 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1071 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1072 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1073 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1074 return serviceIntent;
1075 }
1076
1077 private void clearPrimary(Intent intent) {
1078 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1079 if (dataId == -1) {
1080 Log.e(TAG, "Invalid arguments for clearPrimary request");
1081 return;
1082 }
1083
1084 // Update the primary values in the data record.
1085 ContentValues values = new ContentValues(1);
1086 values.put(Data.IS_SUPER_PRIMARY, 0);
1087 values.put(Data.IS_PRIMARY, 0);
1088
1089 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1090 values, null, null);
1091 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001092
1093 /**
1094 * Creates an intent that can be sent to this service to delete a contact.
1095 */
1096 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1097 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1098 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1099 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1100 return serviceIntent;
1101 }
1102
Brian Attwelld2962a32015-03-02 14:48:50 -08001103 /**
1104 * Creates an intent that can be sent to this service to delete multiple contacts.
1105 */
1106 public static Intent createDeleteMultipleContactsIntent(Context context,
1107 long[] contactIds) {
1108 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1109 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1110 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1111 return serviceIntent;
1112 }
1113
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001114 private void deleteContact(Intent intent) {
1115 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1116 if (contactUri == null) {
1117 Log.e(TAG, "Invalid arguments for deleteContact request");
1118 return;
1119 }
1120
1121 getContentResolver().delete(contactUri, null, null);
1122 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001123
Brian Attwelld2962a32015-03-02 14:48:50 -08001124 private void deleteMultipleContacts(Intent intent) {
1125 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1126 if (contactIds == null) {
1127 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1128 return;
1129 }
1130 for (long contactId : contactIds) {
1131 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1132 getContentResolver().delete(contactUri, null, null);
1133 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001134 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1135 .contacts_deleted_toast, contactIds.length);
1136 mMainHandler.post(new Runnable() {
1137 @Override
1138 public void run() {
1139 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1140 .show();
1141 }
1142 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001143 }
1144
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145 /**
1146 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001147 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001148 */
1149 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001150 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001151 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1152 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1153 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1154 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001155
1156 // Callback intent will be invoked by the service once the contacts are joined.
1157 Intent callbackIntent = new Intent(context, callbackActivity);
1158 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001159 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1160
1161 return serviceIntent;
1162 }
1163
Brian Attwelld3946ca2015-03-03 11:13:49 -08001164 /**
1165 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1166 * No special attention is paid to where the resulting contact's name is taken from.
1167 */
1168 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1169 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1170 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1171 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1172 return serviceIntent;
1173 }
1174
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001175
1176 private interface JoinContactQuery {
1177 String[] PROJECTION = {
1178 RawContacts._ID,
1179 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001180 RawContacts.DISPLAY_NAME_SOURCE,
1181 };
1182
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001183 int _ID = 0;
1184 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001185 int DISPLAY_NAME_SOURCE = 2;
1186 }
1187
1188 private interface ContactEntityQuery {
1189 String[] PROJECTION = {
1190 Contacts.Entity.DATA_ID,
1191 Contacts.Entity.CONTACT_ID,
1192 Contacts.Entity.IS_SUPER_PRIMARY,
1193 };
1194 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1195 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1196 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1197 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1198
1199 int DATA_ID = 0;
1200 int CONTACT_ID = 1;
1201 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001202 }
1203
Brian Attwelld3946ca2015-03-03 11:13:49 -08001204 private void joinSeveralContacts(Intent intent) {
1205 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001206
Brian Attwelld3946ca2015-03-03 11:13:49 -08001207 // Load raw contact IDs for all contacts involved.
1208 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1209 if (rawContactIds == null) {
1210 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001211 return;
1212 }
1213
Brian Attwelld3946ca2015-03-03 11:13:49 -08001214 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001215 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001216 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1217 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1218 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001219 for (int i = 0; i < rawContactIds.length; i++) {
1220 for (int j = 0; j < rawContactIds.length; j++) {
1221 if (i != j) {
1222 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1223 }
Walter Jang0653de32015-07-24 12:12:40 -07001224 // Before we get to 500 we need to flush the operations list
1225 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1226 if (!applyJoinOperations(resolver, operations)) {
1227 return;
1228 }
1229 operations.clear();
1230 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001231 }
1232 }
Walter Jang0653de32015-07-24 12:12:40 -07001233 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1234 return;
1235 }
1236 showToast(R.string.contactsJoinedMessage);
1237 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001238
Walter Jang0653de32015-07-24 12:12:40 -07001239 /** Returns true if the batch was successfully applied and false otherwise. */
1240 private boolean applyJoinOperations(ContentResolver resolver,
1241 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001242 try {
1243 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001244 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001245 } catch (RemoteException | OperationApplicationException e) {
1246 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1247 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001248 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001249 }
1250 }
1251
1252
1253 private void joinContacts(Intent intent) {
1254 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1255 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001256
1257 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001258 // in the join UIs.
1259 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1260 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001261 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001262 return;
1263 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001264
Brian Attwell548f5c62015-01-27 17:46:46 -08001265 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001266
1267 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001268 for (int i = 0; i < rawContactIds.length; i++) {
1269 for (int j = 0; j < rawContactIds.length; j++) {
1270 if (i != j) {
1271 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1272 }
1273 }
1274 }
1275
Brian Attwelld3946ca2015-03-03 11:13:49 -08001276 final ContentResolver resolver = getContentResolver();
1277
Brian Attwell548f5c62015-01-27 17:46:46 -08001278 // Use the name for contactId1 as the name for the newly aggregated contact.
1279 final Uri contactId1Uri = ContentUris.withAppendedId(
1280 Contacts.CONTENT_URI, contactId1);
1281 final Uri entityUri = Uri.withAppendedPath(
1282 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1283 Cursor c = resolver.query(entityUri,
1284 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1285 if (c == null) {
1286 Log.e(TAG, "Unable to open Contacts DB cursor");
1287 showToast(R.string.contactSavedErrorToast);
1288 return;
1289 }
1290 long dataIdToAddSuperPrimary = -1;
1291 try {
1292 if (c.moveToFirst()) {
1293 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1294 }
1295 } finally {
1296 c.close();
1297 }
1298
1299 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1300 // display name does not change as a result of the join.
1301 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001302 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001303 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1304 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1305 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001306 operations.add(builder.build());
1307 }
1308
1309 boolean success = false;
1310 // Apply all aggregation exceptions as one batch
1311 try {
1312 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001313 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001314 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001315 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001316 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001317 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001318 }
1319
1320 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1321 if (success) {
1322 Uri uri = RawContacts.getContactLookupUri(resolver,
1323 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1324 callbackIntent.setData(uri);
1325 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001326 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001327 }
1328
Brian Attwelld3946ca2015-03-03 11:13:49 -08001329 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1330 if (contactIds == null) {
1331 return null;
1332 }
1333
Brian Attwell548f5c62015-01-27 17:46:46 -08001334 final ContentResolver resolver = getContentResolver();
1335 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001336
1337 final StringBuilder queryBuilder = new StringBuilder();
1338 final String stringContactIds[] = new String[contactIds.length];
1339 for (int i = 0; i < contactIds.length; i++) {
1340 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1341 stringContactIds[i] = String.valueOf(contactIds[i]);
1342 if (contactIds[i] == -1) {
1343 return null;
1344 }
1345 if (i == contactIds.length -1) {
1346 break;
1347 }
1348 queryBuilder.append(" OR ");
1349 }
1350
Brian Attwell548f5c62015-01-27 17:46:46 -08001351 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1352 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001353 queryBuilder.toString(),
1354 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001355 if (c == null) {
1356 Log.e(TAG, "Unable to open Contacts DB cursor");
1357 showToast(R.string.contactSavedErrorToast);
1358 return null;
1359 }
1360 try {
1361 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001362 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001363 return null;
1364 }
1365 rawContactIds = new long[c.getCount()];
1366 for (int i = 0; i < rawContactIds.length; i++) {
1367 c.moveToPosition(i);
1368 long rawContactId = c.getLong(JoinContactQuery._ID);
1369 rawContactIds[i] = rawContactId;
1370 }
1371 } finally {
1372 c.close();
1373 }
1374 return rawContactIds;
1375 }
1376
Brian Attwelld3946ca2015-03-03 11:13:49 -08001377 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1378 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1379 }
1380
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001381 /**
1382 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1383 */
1384 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1385 long rawContactId1, long rawContactId2) {
1386 Builder builder =
1387 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1388 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1389 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1390 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1391 operations.add(builder.build());
1392 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001393
1394 /**
1395 * Shows a toast on the UI thread.
1396 */
1397 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001398 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001399
1400 @Override
1401 public void run() {
1402 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1403 }
1404 });
1405 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001406
1407 private void deliverCallback(final Intent callbackIntent) {
1408 mMainHandler.post(new Runnable() {
1409
1410 @Override
1411 public void run() {
1412 deliverCallbackOnUiThread(callbackIntent);
1413 }
1414 });
1415 }
1416
1417 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1418 // TODO: this assumes that if there are multiple instances of the same
1419 // activity registered, the last one registered is the one waiting for
1420 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001421 for (Listener listener : sListeners) {
1422 if (callbackIntent.getComponent().equals(
1423 ((Activity) listener).getIntent().getComponent())) {
1424 listener.onServiceCompleted(callbackIntent);
1425 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001426 }
1427 }
1428 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001429}