blob: bfb684ae23ee92ad4d1c8a66d0299a2f643d3563 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Gary Mai7efa9942016-05-12 11:26:49 -070048import android.support.v4.os.ResultReceiver;
Walter Jang72f99882016-05-26 09:01:31 -070049import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
Wenyi Wangdd7d4562015-12-08 13:33:43 -080053import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080054import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070055import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080056import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080057import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070058import com.android.contacts.common.model.RawContactDelta;
59import com.android.contacts.common.model.RawContactDeltaList;
60import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080061import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070062import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080063import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070064import com.android.contacts.util.ContactPhotoUtils;
65
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070066import com.google.common.collect.Lists;
67import com.google.common.collect.Sets;
68
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080069import java.util.ArrayList;
70import java.util.HashSet;
71import java.util.List;
72import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080074/**
75 * A service responsible for saving changes to the content provider.
76 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070077public class ContactSaveService extends IntentService {
78 private static final String TAG = "ContactSaveService";
79
Katherine Kuana007e442011-07-07 09:25:34 -070080 /** Set to true in order to view logs on content provider operations */
81 private static final boolean DEBUG = false;
82
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
84
85 public static final String EXTRA_ACCOUNT_NAME = "accountName";
86 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070087 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String EXTRA_CONTENT_VALUES = "contentValues";
89 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070090 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
91 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070092
Dmitri Plotnikova0114142011-02-15 13:53:21 -080093 public static final String ACTION_SAVE_CONTACT = "saveContact";
94 public static final String EXTRA_CONTACT_STATE = "state";
95 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070096 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070097 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080098 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070099
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800100 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101 public static final String ACTION_RENAME_GROUP = "renameGroup";
102 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700103 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800104 public static final String EXTRA_GROUP_ID = "groupId";
105 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700106 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
107 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800108
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800109 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800110 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800111 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800112 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800113 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800114 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700115 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800116
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800117 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
118 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
119 public static final String EXTRA_DATA_ID = "dataId";
120
Gary Mai7efa9942016-05-12 11:26:49 -0700121 public static final String ACTION_SPLIT_CONTACT = "splitContact";
122
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800123 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800124 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800125 public static final String EXTRA_CONTACT_ID1 = "contactId1";
126 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800127
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700128 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
129 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
130
131 public static final String ACTION_SET_RINGTONE = "setRingtone";
132 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
133
Gary Mai7efa9942016-05-12 11:26:49 -0700134 public static final int CP2_ERROR = 0;
135 public static final int CONTACTS_LINKED = 1;
136 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700137 public static final int BAD_ARGUMENTS = 3;
Gary Mai7efa9942016-05-12 11:26:49 -0700138
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700139 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
140 Data.MIMETYPE,
141 Data.IS_PRIMARY,
142 Data.DATA1,
143 Data.DATA2,
144 Data.DATA3,
145 Data.DATA4,
146 Data.DATA5,
147 Data.DATA6,
148 Data.DATA7,
149 Data.DATA8,
150 Data.DATA9,
151 Data.DATA10,
152 Data.DATA11,
153 Data.DATA12,
154 Data.DATA13,
155 Data.DATA14,
156 Data.DATA15
157 );
158
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800159 private static final int PERSIST_TRIES = 3;
160
Walter Jang0653de32015-07-24 12:12:40 -0700161 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
162
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800163 public interface Listener {
164 public void onServiceCompleted(Intent callbackIntent);
165 }
166
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100167 private static final CopyOnWriteArrayList<Listener> sListeners =
168 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800169
170 private Handler mMainHandler;
171
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700172 public ContactSaveService() {
173 super(TAG);
174 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800175 mMainHandler = new Handler(Looper.getMainLooper());
176 }
177
178 public static void registerListener(Listener listener) {
179 if (!(listener instanceof Activity)) {
180 throw new ClassCastException("Only activities can be registered to"
181 + " receive callback from " + ContactSaveService.class.getName());
182 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100183 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800184 }
185
186 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100187 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700188 }
189
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800190 /**
191 * Returns true if the ContactSaveService was started successfully and false if an exception
192 * was thrown and a Toast error message was displayed.
193 */
194 public static boolean startService(Context context, Intent intent, int saveMode) {
195 try {
196 context.startService(intent);
197 } catch (Exception exception) {
198 final int resId;
199 switch (saveMode) {
200 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
201 resId = R.string.contactUnlinkErrorToast;
202 break;
203 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
204 resId = R.string.contactJoinErrorToast;
205 break;
206 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
207 resId = R.string.contactSavedErrorToast;
208 break;
209 default:
210 resId = R.string.contactGenericErrorToast;
211 }
212 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
213 return false;
214 }
215 return true;
216 }
217
218 /**
219 * Utility method that starts service and handles exception.
220 */
221 public static void startService(Context context, Intent intent) {
222 try {
223 context.startService(intent);
224 } catch (Exception exception) {
225 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
226 }
227 }
228
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700229 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800230 public Object getSystemService(String name) {
231 Object service = super.getSystemService(name);
232 if (service != null) {
233 return service;
234 }
235
236 return getApplicationContext().getSystemService(name);
237 }
238
239 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700240 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800241 if (intent == null) {
242 Log.d(TAG, "onHandleIntent: could not handle null intent");
243 return;
244 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700245 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
246 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
247 // TODO: add more specific error string such as "Turn on Contacts
248 // permission to update your contacts"
249 showToast(R.string.contactSavedErrorToast);
250 return;
251 }
252
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700253 // Call an appropriate method. If we're sure it affects how incoming phone calls are
254 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700255 String action = intent.getAction();
256 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
257 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800258 } else if (ACTION_SAVE_CONTACT.equals(action)) {
259 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800260 } else if (ACTION_CREATE_GROUP.equals(action)) {
261 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800262 } else if (ACTION_RENAME_GROUP.equals(action)) {
263 renameGroup(intent);
264 } else if (ACTION_DELETE_GROUP.equals(action)) {
265 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700266 } else if (ACTION_UPDATE_GROUP.equals(action)) {
267 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800268 } else if (ACTION_SET_STARRED.equals(action)) {
269 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800270 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
271 setSuperPrimary(intent);
272 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
273 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800274 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
275 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800276 } else if (ACTION_DELETE_CONTACT.equals(action)) {
277 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700278 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
279 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800280 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
281 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800282 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
283 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700284 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
285 setSendToVoicemail(intent);
286 } else if (ACTION_SET_RINGTONE.equals(action)) {
287 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700288 }
289 }
290
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800291 /**
292 * Creates an intent that can be sent to this service to create a new raw contact
293 * using data presented as a set of ContentValues.
294 */
295 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700296 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700297 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800298 Intent serviceIntent = new Intent(
299 context, ContactSaveService.class);
300 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
301 if (account != null) {
302 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
303 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700304 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800305 }
306 serviceIntent.putParcelableArrayListExtra(
307 ContactSaveService.EXTRA_CONTENT_VALUES, values);
308
309 // Callback intent will be invoked by the service once the new contact is
310 // created. The service will put the URI of the new contact as "data" on
311 // the callback intent.
312 Intent callbackIntent = new Intent(context, callbackActivity);
313 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800314 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
315 return serviceIntent;
316 }
317
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700318 private void createRawContact(Intent intent) {
319 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
320 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700321 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700322 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
323 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
324
325 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
326 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
327 .withValue(RawContacts.ACCOUNT_NAME, accountName)
328 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700329 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700330 .build());
331
332 int size = valueList.size();
333 for (int i = 0; i < size; i++) {
334 ContentValues values = valueList.get(i);
335 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
336 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
337 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
338 .withValues(values)
339 .build());
340 }
341
342 ContentResolver resolver = getContentResolver();
343 ContentProviderResult[] results;
344 try {
345 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
346 } catch (Exception e) {
347 throw new RuntimeException("Failed to store new contact", e);
348 }
349
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700350 Uri rawContactUri = results[0].uri;
351 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
352
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800353 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700354 }
355
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700356 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800357 * Creates an intent that can be sent to this service to create a new raw contact
358 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800359 * This variant is more convenient to use when there is only one photo that can
360 * possibly be updated, as in the Contact Details screen.
361 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
362 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800363 */
Maurice Chu851222a2012-06-21 11:43:08 -0700364 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700365 String saveModeExtraKey, int saveMode, boolean isProfile,
366 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700367 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800368 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700369 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800370 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700371 callbackActivity, callbackAction, bundle,
372 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800373 }
374
375 /**
376 * Creates an intent that can be sent to this service to create a new raw contact
377 * using data presented as a set of ContentValues.
378 * This variant is used when multiple contacts' photos may be updated, as in the
379 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700380 *
Josh Garguse692e012012-01-18 14:53:11 -0800381 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700382 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
383 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800384 */
Maurice Chu851222a2012-06-21 11:43:08 -0700385 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700386 String saveModeExtraKey, int saveMode, boolean isProfile,
387 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700388 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000389 Intent serviceIntent = new Intent(
390 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800391 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
392 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700393 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800394 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
395
Josh Garguse692e012012-01-18 14:53:11 -0800396 if (updatedPhotos != null) {
397 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
398 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800399
Josh Garguse5d3f892012-04-11 11:56:15 -0700400 if (callbackActivity != null) {
401 // Callback intent will be invoked by the service once the contact is
402 // saved. The service will put the URI of the new contact as "data" on
403 // the callback intent.
404 Intent callbackIntent = new Intent(context, callbackActivity);
405 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700406 if (joinContactIdExtraKey != null && joinContactId != null) {
407 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
408 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700409 callbackIntent.setAction(callbackAction);
410 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
411 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800412 return serviceIntent;
413 }
414
415 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700416 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700417 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800418 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800419
Jay Shrauner08099782015-03-25 14:17:11 -0700420 if (state == null) {
421 Log.e(TAG, "Invalid arguments for saveContact request");
422 return;
423 }
424
benny.lin3a4e7a22014-01-08 10:58:08 +0800425 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800426 // Trim any empty fields, and RawContacts, before persisting
427 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700428 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800429
430 Uri lookupUri = null;
431
432 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700433
Josh Garguse692e012012-01-18 14:53:11 -0800434 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800435
Josh Gargusef15c8e2012-01-30 16:42:02 -0800436 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
437 long insertedRawContactId = -1;
438
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800439 // Attempt to persist changes
440 int tries = 0;
441 while (tries++ < PERSIST_TRIES) {
442 try {
443 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800444 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
445
446 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
447
448 for (CPOWrapper cpoWrapper : diffWrapper) {
449 diff.add(cpoWrapper.getOperation());
450 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700451
Katherine Kuana007e442011-07-07 09:25:34 -0700452 if (DEBUG) {
453 Log.v(TAG, "Content Provider Operations:");
454 for (ContentProviderOperation operation : diff) {
455 Log.v(TAG, operation.toString());
456 }
457 }
458
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700459 int numberProcessed = 0;
460 boolean batchFailed = false;
461 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
462 while (numberProcessed < diff.size()) {
463 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
464 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700465 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700466 batchFailed = true;
467 break;
468 } else {
469 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700470 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800471 }
472
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700473 if (batchFailed) {
474 // Retry save
475 continue;
476 }
477
Wenyi Wang67addcc2015-11-23 10:07:48 -0800478 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800479 if (rawContactId == -1) {
480 throw new IllegalStateException("Could not determine RawContact ID after save");
481 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800482 // We don't have to check to see if the value is still -1. If we reach here,
483 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800484 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700485 if (isProfile) {
486 // Since the profile supports local raw contacts, which may have been completely
487 // removed if all information was removed, we need to do a special query to
488 // get the lookup URI for the profile contact (if it still exists).
489 Cursor c = resolver.query(Profile.CONTENT_URI,
490 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
491 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800492 if (c == null) {
493 continue;
494 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700495 try {
Erik162b7e32011-09-20 15:23:55 -0700496 if (c.moveToFirst()) {
497 final long contactId = c.getLong(0);
498 final String lookupKey = c.getString(1);
499 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
500 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700501 } finally {
502 c.close();
503 }
504 } else {
505 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
506 rawContactId);
507 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
508 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800509 if (lookupUri != null) {
510 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
511 }
Josh Garguse692e012012-01-18 14:53:11 -0800512
513 // We can change this back to false later, if we fail to save the contact photo.
514 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800515 break;
516
517 } catch (RemoteException e) {
518 // Something went wrong, bail without success
519 Log.e(TAG, "Problem persisting user edits", e);
520 break;
521
Jay Shrauner57fca182014-01-17 14:20:50 -0800522 } catch (IllegalArgumentException e) {
523 // This is thrown by applyBatch on malformed requests
524 Log.e(TAG, "Problem persisting user edits", e);
525 showToast(R.string.contactSavedErrorToast);
526 break;
527
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800528 } catch (OperationApplicationException e) {
529 // Version consistency failed, re-parent change and try again
530 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
531 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
532 boolean first = true;
533 final int count = state.size();
534 for (int i = 0; i < count; i++) {
535 Long rawContactId = state.getRawContactId(i);
536 if (rawContactId != null && rawContactId != -1) {
537 if (!first) {
538 sb.append(',');
539 }
540 sb.append(rawContactId);
541 first = false;
542 }
543 }
544 sb.append(")");
545
546 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800547 throw new IllegalStateException(
548 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800549 }
550
Maurice Chu851222a2012-06-21 11:43:08 -0700551 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700552 isProfile
553 ? RawContactsEntity.PROFILE_CONTENT_URI
554 : RawContactsEntity.CONTENT_URI,
555 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700556 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700557
558 // Update the new state to use profile URIs if appropriate.
559 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700560 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700561 delta.setProfileQueryUri();
562 }
563 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800564 }
565 }
566
Josh Garguse692e012012-01-18 14:53:11 -0800567 // Now save any updated photos. We do this at the end to ensure that
568 // the ContactProvider already knows about newly-created contacts.
569 if (updatedPhotos != null) {
570 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700571 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800572 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800573
574 // If the raw-contact ID is negative, we are saving a new raw-contact;
575 // replace the bogus ID with the new one that we actually saved the contact at.
576 if (rawContactId < 0) {
577 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800578 }
579
Jay Shrauner511561d2015-04-02 10:35:33 -0700580 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700581 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700582 succeeded = false;
583 }
Josh Garguse692e012012-01-18 14:53:11 -0800584 }
585 }
586
Josh Garguse5d3f892012-04-11 11:56:15 -0700587 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
588 if (callbackIntent != null) {
589 if (succeeded) {
590 // Mark the intent to indicate that the save was successful (even if the lookup URI
591 // is now null). For local contacts or the local profile, it's possible that the
592 // save triggered removal of the contact, so no lookup URI would exist..
593 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
594 }
595 callbackIntent.setData(lookupUri);
596 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800597 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800598 }
599
Josh Garguse692e012012-01-18 14:53:11 -0800600 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700601 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
602 * subsets, adds the returned array to "results".
603 *
604 * @return the size of the array, if not null; -1 when the array is null.
605 */
606 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
607 ContentProviderResult[] results, ContentResolver resolver)
608 throws RemoteException, OperationApplicationException {
609 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
610 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
611 subset.addAll(diff.subList(offset, offset + subsetCount));
612 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
613 .AUTHORITY, subset);
614 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
615 return -1;
616 }
617 for (ContentProviderResult c : subsetResult) {
618 results[offset++] = c;
619 }
620 return subsetResult.length;
621 }
622
623 /**
Josh Garguse692e012012-01-18 14:53:11 -0800624 * Save updated photo for the specified raw-contact.
625 * @return true for success, false for failure
626 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800627 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800628 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800629 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
630 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
631
benny.lin3a4e7a22014-01-08 10:58:08 +0800632 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800633 }
634
Josh Gargusef15c8e2012-01-30 16:42:02 -0800635 /**
636 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
637 */
Maurice Chu851222a2012-06-21 11:43:08 -0700638 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800639 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800640 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800641 long existingRawContactId = state.findRawContactId();
642 if (existingRawContactId != -1) {
643 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800644 }
645
Wenyi Wang67addcc2015-11-23 10:07:48 -0800646 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800647 }
648
649 /**
650 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
651 */
652 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800653 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800654 if (results == null) {
655 return -1;
656 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800657 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800658 final int numResults = results.length;
659 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800660 final CPOWrapper cpoWrapper = diffWrapper.get(i);
661 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
662 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
663 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800664 return ContentUris.parseId(results[i].uri);
665 }
666 }
667 return -1;
668 }
669
670 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700671 * Creates an intent that can be sent to this service to create a new group as
672 * well as add new members at the same time.
673 *
674 * @param context of the application
675 * @param account in which the group should be created
676 * @param label is the name of the group (cannot be null)
677 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
678 * should be added to the group
679 * @param callbackActivity is the activity to send the callback intent to
680 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700681 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700682 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700683 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700684 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800685 Intent serviceIntent = new Intent(context, ContactSaveService.class);
686 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700689 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800690 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700691 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700692
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800693 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700694 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800695 Intent callbackIntent = new Intent(context, callbackActivity);
696 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700697 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800698
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700699 return serviceIntent;
700 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800701
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800702 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700703 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
704 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
705 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
706 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700707 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800708
709 ContentValues values = new ContentValues();
710 values.put(Groups.ACCOUNT_TYPE, accountType);
711 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700712 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800713 values.put(Groups.TITLE, label);
714
Katherine Kuan717e3432011-07-13 17:03:24 -0700715 final ContentResolver resolver = getContentResolver();
716
717 // Create the new group
718 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
719
720 // If there's no URI, then the insertion failed. Abort early because group members can't be
721 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800722 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700723 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800724 return;
725 }
726
Katherine Kuan717e3432011-07-13 17:03:24 -0700727 // Add new group members
728 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
729
730 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
731 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800732 values.clear();
733 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
734 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
735
736 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700737 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800739 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800740 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800741 }
742
743 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800744 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800745 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700746 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700747 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800748 Intent serviceIntent = new Intent(context, ContactSaveService.class);
749 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
750 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
751 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700752
753 // Callback intent will be invoked by the service once the group is renamed.
754 Intent callbackIntent = new Intent(context, callbackActivity);
755 callbackIntent.setAction(callbackAction);
756 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
757
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800758 return serviceIntent;
759 }
760
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800761 private void renameGroup(Intent intent) {
762 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
763 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
764
765 if (groupId == -1) {
766 Log.e(TAG, "Invalid arguments for renameGroup request");
767 return;
768 }
769
770 ContentValues values = new ContentValues();
771 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700772 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
773 getContentResolver().update(groupUri, values, null, null);
774
775 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
776 callbackIntent.setData(groupUri);
777 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800778 }
779
780 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800781 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800782 */
Walter Jang72f99882016-05-26 09:01:31 -0700783 public static Intent createGroupDeletionIntent(Context context, long groupId,
784 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800785 Intent serviceIntent = new Intent(context, ContactSaveService.class);
786 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800787 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700788
789 // Callback intent will be invoked by the service once the group is updated
790 if (callbackActivity != null && !TextUtils.isEmpty(callbackAction)) {
791 final Intent callbackIntent = new Intent(context, callbackActivity);
792 callbackIntent.setAction(callbackAction);
793 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
794 }
795
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800796 return serviceIntent;
797 }
798
799 private void deleteGroup(Intent intent) {
800 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
801 if (groupId == -1) {
802 Log.e(TAG, "Invalid arguments for deleteGroup request");
803 return;
804 }
805
806 getContentResolver().delete(
807 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
Walter Jang72f99882016-05-26 09:01:31 -0700808
809 final Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
810 if (callbackIntent != null) {
811 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
812 callbackIntent.setData(groupUri);
813 deliverCallback(callbackIntent);
814 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800815 }
816
817 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700818 * Creates an intent that can be sent to this service to rename a group as
819 * well as add and remove members from the group.
820 *
821 * @param context of the application
822 * @param groupId of the group that should be modified
823 * @param newLabel is the updated name of the group (can be null if the name
824 * should not be updated)
825 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
826 * should be added to the group
827 * @param rawContactsToRemove is an array of raw contact IDs for contacts
828 * that should be removed from the group
829 * @param callbackActivity is the activity to send the callback intent to
830 * @param callbackAction is the intent action for the callback intent
831 */
832 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
833 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700834 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700835 Intent serviceIntent = new Intent(context, ContactSaveService.class);
836 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
837 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
838 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
839 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
840 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
841 rawContactsToRemove);
842
843 // Callback intent will be invoked by the service once the group is updated
844 Intent callbackIntent = new Intent(context, callbackActivity);
845 callbackIntent.setAction(callbackAction);
846 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
847
848 return serviceIntent;
849 }
850
851 private void updateGroup(Intent intent) {
852 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
853 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
854 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
855 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
856
857 if (groupId == -1) {
858 Log.e(TAG, "Invalid arguments for updateGroup request");
859 return;
860 }
861
862 final ContentResolver resolver = getContentResolver();
863 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
864
865 // Update group name if necessary
866 if (label != null) {
867 ContentValues values = new ContentValues();
868 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700869 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700870 }
871
Katherine Kuan717e3432011-07-13 17:03:24 -0700872 // Add and remove members if necessary
873 addMembersToGroup(resolver, rawContactsToAdd, groupId);
874 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
875
876 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
877 callbackIntent.setData(groupUri);
878 deliverCallback(callbackIntent);
879 }
880
Daniel Lehmann18958a22012-02-28 17:45:25 -0800881 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700882 long groupId) {
883 if (rawContactsToAdd == null) {
884 return;
885 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700886 for (long rawContactId : rawContactsToAdd) {
887 try {
888 final ArrayList<ContentProviderOperation> rawContactOperations =
889 new ArrayList<ContentProviderOperation>();
890
891 // Build an assert operation to ensure the contact is not already in the group
892 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
893 .newAssertQuery(Data.CONTENT_URI);
894 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
895 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
896 new String[] { String.valueOf(rawContactId),
897 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
898 assertBuilder.withExpectedCount(0);
899 rawContactOperations.add(assertBuilder.build());
900
901 // Build an insert operation to add the contact to the group
902 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
903 .newInsert(Data.CONTENT_URI);
904 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
905 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
906 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
907 rawContactOperations.add(insertBuilder.build());
908
909 if (DEBUG) {
910 for (ContentProviderOperation operation : rawContactOperations) {
911 Log.v(TAG, operation.toString());
912 }
913 }
914
915 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700916 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800917 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700918 }
919 } catch (RemoteException e) {
920 // Something went wrong, bail without success
921 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
922 String.valueOf(rawContactId), e);
923 } catch (OperationApplicationException e) {
924 // The assert could have failed because the contact is already in the group,
925 // just continue to the next contact
926 Log.w(TAG, "Assert failed in adding raw contact ID " +
927 String.valueOf(rawContactId) + ". Already exists in group " +
928 String.valueOf(groupId), e);
929 }
930 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700931 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700932
Daniel Lehmann18958a22012-02-28 17:45:25 -0800933 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700934 long groupId) {
935 if (rawContactsToRemove == null) {
936 return;
937 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700938 for (long rawContactId : rawContactsToRemove) {
939 // Apply the delete operation on the data row for the given raw contact's
940 // membership in the given group. If no contact matches the provided selection, then
941 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800942 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700943 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
944 new String[] { String.valueOf(rawContactId),
945 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
946 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700947 }
948
949 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800950 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800951 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800952 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
953 Intent serviceIntent = new Intent(context, ContactSaveService.class);
954 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
955 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
956 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
957
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800958 return serviceIntent;
959 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800960
961 private void setStarred(Intent intent) {
962 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
963 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
964 if (contactUri == null) {
965 Log.e(TAG, "Invalid arguments for setStarred request");
966 return;
967 }
968
969 final ContentValues values = new ContentValues(1);
970 values.put(Contacts.STARRED, value);
971 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700972
973 // Undemote the contact if necessary
974 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
975 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800976 if (c == null) {
977 return;
978 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700979 try {
980 if (c.moveToFirst()) {
981 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700982
983 // Don't bother undemoting if this contact is the user's profile.
984 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800985 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700986 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700987 }
988 } finally {
989 c.close();
990 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800991 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800992
993 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700994 * Creates an intent that can be sent to this service to set the redirect to voicemail.
995 */
996 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
997 boolean value) {
998 Intent serviceIntent = new Intent(context, ContactSaveService.class);
999 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1000 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1001 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1002
1003 return serviceIntent;
1004 }
1005
1006 private void setSendToVoicemail(Intent intent) {
1007 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1008 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1009 if (contactUri == null) {
1010 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1011 return;
1012 }
1013
1014 final ContentValues values = new ContentValues(1);
1015 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1016 getContentResolver().update(contactUri, values, null, null);
1017 }
1018
1019 /**
1020 * Creates an intent that can be sent to this service to save the contact's ringtone.
1021 */
1022 public static Intent createSetRingtone(Context context, Uri contactUri,
1023 String value) {
1024 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1025 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1026 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1027 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1028
1029 return serviceIntent;
1030 }
1031
1032 private void setRingtone(Intent intent) {
1033 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1034 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1035 if (contactUri == null) {
1036 Log.e(TAG, "Invalid arguments for setRingtone");
1037 return;
1038 }
1039 ContentValues values = new ContentValues(1);
1040 values.put(Contacts.CUSTOM_RINGTONE, value);
1041 getContentResolver().update(contactUri, values, null, null);
1042 }
1043
1044 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001045 * Creates an intent that sets the selected data item as super primary (default)
1046 */
1047 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1048 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1049 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1050 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1051 return serviceIntent;
1052 }
1053
1054 private void setSuperPrimary(Intent intent) {
1055 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1056 if (dataId == -1) {
1057 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1058 return;
1059 }
1060
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001061 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001062 }
1063
1064 /**
1065 * Creates an intent that clears the primary flag of all data items that belong to the same
1066 * raw_contact as the given data item. Will only clear, if the data item was primary before
1067 * this call
1068 */
1069 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1070 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1071 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1072 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1073 return serviceIntent;
1074 }
1075
1076 private void clearPrimary(Intent intent) {
1077 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1078 if (dataId == -1) {
1079 Log.e(TAG, "Invalid arguments for clearPrimary request");
1080 return;
1081 }
1082
1083 // Update the primary values in the data record.
1084 ContentValues values = new ContentValues(1);
1085 values.put(Data.IS_SUPER_PRIMARY, 0);
1086 values.put(Data.IS_PRIMARY, 0);
1087
1088 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1089 values, null, null);
1090 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001091
1092 /**
1093 * Creates an intent that can be sent to this service to delete a contact.
1094 */
1095 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1096 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1097 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1098 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1099 return serviceIntent;
1100 }
1101
Brian Attwelld2962a32015-03-02 14:48:50 -08001102 /**
1103 * Creates an intent that can be sent to this service to delete multiple contacts.
1104 */
1105 public static Intent createDeleteMultipleContactsIntent(Context context,
1106 long[] contactIds) {
1107 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1108 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1109 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1110 return serviceIntent;
1111 }
1112
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001113 private void deleteContact(Intent intent) {
1114 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1115 if (contactUri == null) {
1116 Log.e(TAG, "Invalid arguments for deleteContact request");
1117 return;
1118 }
1119
1120 getContentResolver().delete(contactUri, null, null);
1121 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001122
Brian Attwelld2962a32015-03-02 14:48:50 -08001123 private void deleteMultipleContacts(Intent intent) {
1124 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1125 if (contactIds == null) {
1126 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1127 return;
1128 }
1129 for (long contactId : contactIds) {
1130 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1131 getContentResolver().delete(contactUri, null, null);
1132 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001133 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1134 .contacts_deleted_toast, contactIds.length);
1135 mMainHandler.post(new Runnable() {
1136 @Override
1137 public void run() {
1138 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1139 .show();
1140 }
1141 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001142 }
1143
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001144 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001145 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Mai53fe0d22016-07-26 17:23:53 -07001146 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1147 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001148 */
1149 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1150 ResultReceiver receiver) {
1151 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1152 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1153 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1154 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1155 return serviceIntent;
1156 }
1157
1158 private void splitContact(Intent intent) {
1159 final long rawContactIds[][] = (long[][]) intent
1160 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001161 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001162 if (rawContactIds == null) {
1163 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001164 if (receiver != null) {
1165 receiver.send(BAD_ARGUMENTS, new Bundle());
1166 }
Gary Mai7efa9942016-05-12 11:26:49 -07001167 return;
1168 }
1169 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1170 final ContentResolver resolver = getContentResolver();
1171 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001172 for (int i = 0; i < rawContactIds.length; i++) {
1173 for (int j = 0; j < rawContactIds.length; j++) {
1174 if (i != j) {
1175 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1176 if (receiver != null) {
1177 receiver.send(CP2_ERROR, new Bundle());
1178 return;
1179 }
1180 }
1181 }
1182 }
1183 }
1184 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1185 if (receiver != null) {
1186 receiver.send(CP2_ERROR, new Bundle());
1187 }
1188 return;
1189 }
1190 if (receiver != null) {
1191 receiver.send(CONTACTS_SPLIT, new Bundle());
1192 } else {
1193 showToast(R.string.contactUnlinkedToast);
1194 }
1195 }
1196
1197 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001198 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001199 * and {@param rawContactIds2} to {@param operations}.
1200 * @return false if an error occurred, true otherwise.
1201 */
1202 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1203 long[] rawContactIds1, long[] rawContactIds2) {
1204 if (rawContactIds1 == null || rawContactIds2 == null) {
1205 Log.e(TAG, "Invalid arguments for splitContact request");
1206 return false;
1207 }
1208 // For each pair of raw contacts, insert an aggregation exception
1209 final ContentResolver resolver = getContentResolver();
1210 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1211 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1212 for (int i = 0; i < rawContactIds1.length; i++) {
1213 for (int j = 0; j < rawContactIds2.length; j++) {
1214 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1215 // Before we get to 500 we need to flush the operations list
1216 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1217 if (!applyOperations(resolver, operations)) {
1218 return false;
1219 }
1220 operations.clear();
1221 }
1222 }
1223 }
1224 return true;
1225 }
1226
1227 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001228 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001229 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001230 */
1231 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001232 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001233 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1234 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1235 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1236 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001237
1238 // Callback intent will be invoked by the service once the contacts are joined.
1239 Intent callbackIntent = new Intent(context, callbackActivity);
1240 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001241 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1242
1243 return serviceIntent;
1244 }
1245
Brian Attwelld3946ca2015-03-03 11:13:49 -08001246 /**
1247 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1248 * No special attention is paid to where the resulting contact's name is taken from.
1249 */
Gary Mai7efa9942016-05-12 11:26:49 -07001250 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1251 ResultReceiver receiver) {
1252 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001253 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1254 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001255 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001256 return serviceIntent;
1257 }
1258
Gary Mai7efa9942016-05-12 11:26:49 -07001259 /**
1260 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1261 * No special attention is paid to where the resulting contact's name is taken from.
1262 */
1263 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1264 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1265 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001266
1267 private interface JoinContactQuery {
1268 String[] PROJECTION = {
1269 RawContacts._ID,
1270 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001271 RawContacts.DISPLAY_NAME_SOURCE,
1272 };
1273
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001274 int _ID = 0;
1275 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001276 int DISPLAY_NAME_SOURCE = 2;
1277 }
1278
1279 private interface ContactEntityQuery {
1280 String[] PROJECTION = {
1281 Contacts.Entity.DATA_ID,
1282 Contacts.Entity.CONTACT_ID,
1283 Contacts.Entity.IS_SUPER_PRIMARY,
1284 };
1285 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1286 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1287 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1288 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1289
1290 int DATA_ID = 0;
1291 int CONTACT_ID = 1;
1292 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001293 }
1294
Brian Attwelld3946ca2015-03-03 11:13:49 -08001295 private void joinSeveralContacts(Intent intent) {
1296 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001297
Gary Mai7efa9942016-05-12 11:26:49 -07001298 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001299
Brian Attwelld3946ca2015-03-03 11:13:49 -08001300 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001301 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1302 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001303 if (rawContactIds == null) {
1304 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001305 if (receiver != null) {
1306 receiver.send(BAD_ARGUMENTS, new Bundle());
1307 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001308 return;
1309 }
1310
Brian Attwelld3946ca2015-03-03 11:13:49 -08001311 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001312 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001313 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1314 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1315 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001316 for (int i = 0; i < rawContactIds.length; i++) {
1317 for (int j = 0; j < rawContactIds.length; j++) {
1318 if (i != j) {
1319 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1320 }
Walter Jang0653de32015-07-24 12:12:40 -07001321 // Before we get to 500 we need to flush the operations list
1322 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001323 if (!applyOperations(resolver, operations)) {
1324 if (receiver != null) {
1325 receiver.send(CP2_ERROR, new Bundle());
1326 }
Walter Jang0653de32015-07-24 12:12:40 -07001327 return;
1328 }
1329 operations.clear();
1330 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001331 }
1332 }
Gary Mai7efa9942016-05-12 11:26:49 -07001333 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1334 if (receiver != null) {
1335 receiver.send(CP2_ERROR, new Bundle());
1336 }
Walter Jang0653de32015-07-24 12:12:40 -07001337 return;
1338 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001339
Gary Mai7efa9942016-05-12 11:26:49 -07001340 if (receiver != null) {
1341 final Bundle result = new Bundle();
1342 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001343 result.putString(EXTRA_DISPLAY_NAME, queryNameOfLinkedContacts(contactIds));
Gary Mai7efa9942016-05-12 11:26:49 -07001344 receiver.send(CONTACTS_LINKED, result);
1345 } else {
1346 showToast(R.string.contactsJoinedMessage);
1347 }
Walter Jang0653de32015-07-24 12:12:40 -07001348 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001349
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001350 // Get the display name of the top-level contact after the contacts have been linked.
1351 private String queryNameOfLinkedContacts(long[] contactIds) {
1352 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1353 final String[] whereArgs = new String[contactIds.length];
1354 for (int i = 0; i < contactIds.length; i++) {
1355 whereArgs[i] = String.valueOf(contactIds[i]);
1356 whereBuilder.append("?,");
1357 }
1358 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1359 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
1360 new String[]{Contacts.DISPLAY_NAME}, whereBuilder.toString(), whereArgs, null);
1361 try {
1362 if (cursor.moveToFirst()) {
1363 return cursor.getString(0);
1364 }
1365 return null;
1366 } finally {
1367 cursor.close();
1368 }
1369 }
1370
1371
Walter Jang0653de32015-07-24 12:12:40 -07001372 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001373 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001374 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001375 try {
1376 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001377 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001378 } catch (RemoteException | OperationApplicationException e) {
1379 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1380 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001381 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001382 }
1383 }
1384
Brian Attwelld3946ca2015-03-03 11:13:49 -08001385 private void joinContacts(Intent intent) {
1386 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1387 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001388
1389 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001390 // in the join UIs.
1391 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1392 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001393 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001394 return;
1395 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001396
Brian Attwell548f5c62015-01-27 17:46:46 -08001397 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001398
1399 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001400 for (int i = 0; i < rawContactIds.length; i++) {
1401 for (int j = 0; j < rawContactIds.length; j++) {
1402 if (i != j) {
1403 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1404 }
1405 }
1406 }
1407
Brian Attwelld3946ca2015-03-03 11:13:49 -08001408 final ContentResolver resolver = getContentResolver();
1409
Brian Attwell548f5c62015-01-27 17:46:46 -08001410 // Use the name for contactId1 as the name for the newly aggregated contact.
1411 final Uri contactId1Uri = ContentUris.withAppendedId(
1412 Contacts.CONTENT_URI, contactId1);
1413 final Uri entityUri = Uri.withAppendedPath(
1414 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1415 Cursor c = resolver.query(entityUri,
1416 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1417 if (c == null) {
1418 Log.e(TAG, "Unable to open Contacts DB cursor");
1419 showToast(R.string.contactSavedErrorToast);
1420 return;
1421 }
1422 long dataIdToAddSuperPrimary = -1;
1423 try {
1424 if (c.moveToFirst()) {
1425 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1426 }
1427 } finally {
1428 c.close();
1429 }
1430
1431 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1432 // display name does not change as a result of the join.
1433 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001434 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001435 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1436 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1437 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001438 operations.add(builder.build());
1439 }
1440
1441 boolean success = false;
1442 // Apply all aggregation exceptions as one batch
1443 try {
1444 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001445 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001446 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001447 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001448 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001449 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001450 }
1451
1452 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1453 if (success) {
1454 Uri uri = RawContacts.getContactLookupUri(resolver,
1455 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1456 callbackIntent.setData(uri);
1457 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001458 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001459 }
1460
Gary Mai7efa9942016-05-12 11:26:49 -07001461 /**
1462 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1463 * array of the return value holds an array of raw contact ids for one contactId.
1464 * @param contactIds
1465 * @return
1466 */
1467 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1468 final long[][] rawContactIds = new long[contactIds.length][];
1469 for (int i = 0; i < contactIds.length; i++) {
1470 rawContactIds[i] = getRawContactIds(contactIds[i]);
1471 }
1472 return rawContactIds;
1473 }
1474
1475 /**
1476 * Gets the raw contact ids associated with {@param contactId}.
1477 * @param contactId
1478 * @return Array of raw contact ids.
1479 */
1480 private long[] getRawContactIds(long contactId) {
1481 final ContentResolver resolver = getContentResolver();
1482 long rawContactIds[];
1483
1484 final StringBuilder queryBuilder = new StringBuilder();
1485 queryBuilder.append(RawContacts.CONTACT_ID)
1486 .append("=")
1487 .append(String.valueOf(contactId));
1488
1489 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1490 JoinContactQuery.PROJECTION,
1491 queryBuilder.toString(),
1492 null, null);
1493 if (c == null) {
1494 Log.e(TAG, "Unable to open Contacts DB cursor");
1495 return null;
1496 }
1497 try {
1498 rawContactIds = new long[c.getCount()];
1499 for (int i = 0; i < rawContactIds.length; i++) {
1500 c.moveToPosition(i);
1501 final long rawContactId = c.getLong(JoinContactQuery._ID);
1502 rawContactIds[i] = rawContactId;
1503 }
1504 } finally {
1505 c.close();
1506 }
1507 return rawContactIds;
1508 }
1509
Brian Attwelld3946ca2015-03-03 11:13:49 -08001510 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1511 if (contactIds == null) {
1512 return null;
1513 }
1514
Brian Attwell548f5c62015-01-27 17:46:46 -08001515 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001516
1517 final StringBuilder queryBuilder = new StringBuilder();
1518 final String stringContactIds[] = new String[contactIds.length];
1519 for (int i = 0; i < contactIds.length; i++) {
1520 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1521 stringContactIds[i] = String.valueOf(contactIds[i]);
1522 if (contactIds[i] == -1) {
1523 return null;
1524 }
1525 if (i == contactIds.length -1) {
1526 break;
1527 }
1528 queryBuilder.append(" OR ");
1529 }
1530
Brian Attwell548f5c62015-01-27 17:46:46 -08001531 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1532 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001533 queryBuilder.toString(),
1534 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001535 if (c == null) {
1536 Log.e(TAG, "Unable to open Contacts DB cursor");
1537 showToast(R.string.contactSavedErrorToast);
1538 return null;
1539 }
Gary Mai7efa9942016-05-12 11:26:49 -07001540 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001541 try {
1542 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001543 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001544 return null;
1545 }
1546 rawContactIds = new long[c.getCount()];
1547 for (int i = 0; i < rawContactIds.length; i++) {
1548 c.moveToPosition(i);
1549 long rawContactId = c.getLong(JoinContactQuery._ID);
1550 rawContactIds[i] = rawContactId;
1551 }
1552 } finally {
1553 c.close();
1554 }
1555 return rawContactIds;
1556 }
1557
Brian Attwelld3946ca2015-03-03 11:13:49 -08001558 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1559 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1560 }
1561
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001562 /**
1563 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1564 */
1565 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1566 long rawContactId1, long rawContactId2) {
1567 Builder builder =
1568 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1569 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1570 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1571 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1572 operations.add(builder.build());
1573 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001574
1575 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001576 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001577 */
1578 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1579 long rawContactId1, long rawContactId2) {
1580 final Builder builder =
1581 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001582 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001583 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1584 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1585 operations.add(builder.build());
1586 }
1587
1588 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001589 * Shows a toast on the UI thread.
1590 */
1591 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001592 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001593
1594 @Override
1595 public void run() {
1596 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1597 }
1598 });
1599 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001600
1601 private void deliverCallback(final Intent callbackIntent) {
1602 mMainHandler.post(new Runnable() {
1603
1604 @Override
1605 public void run() {
1606 deliverCallbackOnUiThread(callbackIntent);
1607 }
1608 });
1609 }
1610
1611 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1612 // TODO: this assumes that if there are multiple instances of the same
1613 // activity registered, the last one registered is the one waiting for
1614 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001615 for (Listener listener : sListeners) {
1616 if (callbackIntent.getComponent().equals(
1617 ((Activity) listener).getIntent().getComponent())) {
1618 listener.onServiceCompleted(callbackIntent);
1619 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001620 }
1621 }
1622 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001623}