blob: b68d34cd2e21ec476bd0a34496289264ba6cb5dc [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Gary Mai7efa9942016-05-12 11:26:49 -070048import android.support.v4.os.ResultReceiver;
Walter Jang72f99882016-05-26 09:01:31 -070049import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
Wenyi Wangdd7d4562015-12-08 13:33:43 -080053import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080054import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070055import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080056import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080057import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070058import com.android.contacts.common.model.RawContactDelta;
59import com.android.contacts.common.model.RawContactDeltaList;
60import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080061import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070062import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080063import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070064import com.android.contacts.util.ContactPhotoUtils;
65
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070066import com.google.common.collect.Lists;
67import com.google.common.collect.Sets;
68
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080069import java.util.ArrayList;
70import java.util.HashSet;
71import java.util.List;
72import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080074/**
75 * A service responsible for saving changes to the content provider.
76 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070077public class ContactSaveService extends IntentService {
78 private static final String TAG = "ContactSaveService";
79
Katherine Kuana007e442011-07-07 09:25:34 -070080 /** Set to true in order to view logs on content provider operations */
81 private static final boolean DEBUG = false;
82
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
84
85 public static final String EXTRA_ACCOUNT_NAME = "accountName";
86 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070087 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String EXTRA_CONTENT_VALUES = "contentValues";
89 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070090 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
91 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070092
Dmitri Plotnikova0114142011-02-15 13:53:21 -080093 public static final String ACTION_SAVE_CONTACT = "saveContact";
94 public static final String EXTRA_CONTACT_STATE = "state";
95 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070096 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070097 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080098 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070099
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800100 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101 public static final String ACTION_RENAME_GROUP = "renameGroup";
102 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700103 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800104 public static final String EXTRA_GROUP_ID = "groupId";
105 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700106 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
107 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800108
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800109 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800110 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800111 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800112 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800113 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800114 public static final String EXTRA_STARRED_FLAG = "starred";
115
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800116 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
117 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
118 public static final String EXTRA_DATA_ID = "dataId";
119
Gary Mai7efa9942016-05-12 11:26:49 -0700120 public static final String ACTION_SPLIT_CONTACT = "splitContact";
121
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800122 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800123 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800124 public static final String EXTRA_CONTACT_ID1 = "contactId1";
125 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800126
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700127 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
128 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
129
130 public static final String ACTION_SET_RINGTONE = "setRingtone";
131 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
132
Gary Mai7efa9942016-05-12 11:26:49 -0700133 public static final int CP2_ERROR = 0;
134 public static final int CONTACTS_LINKED = 1;
135 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700136 public static final int BAD_ARGUMENTS = 3;
Gary Mai7efa9942016-05-12 11:26:49 -0700137
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700138 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
139 Data.MIMETYPE,
140 Data.IS_PRIMARY,
141 Data.DATA1,
142 Data.DATA2,
143 Data.DATA3,
144 Data.DATA4,
145 Data.DATA5,
146 Data.DATA6,
147 Data.DATA7,
148 Data.DATA8,
149 Data.DATA9,
150 Data.DATA10,
151 Data.DATA11,
152 Data.DATA12,
153 Data.DATA13,
154 Data.DATA14,
155 Data.DATA15
156 );
157
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800158 private static final int PERSIST_TRIES = 3;
159
Walter Jang0653de32015-07-24 12:12:40 -0700160 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
161
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800162 public interface Listener {
163 public void onServiceCompleted(Intent callbackIntent);
164 }
165
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100166 private static final CopyOnWriteArrayList<Listener> sListeners =
167 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800168
169 private Handler mMainHandler;
170
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700171 public ContactSaveService() {
172 super(TAG);
173 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800174 mMainHandler = new Handler(Looper.getMainLooper());
175 }
176
177 public static void registerListener(Listener listener) {
178 if (!(listener instanceof Activity)) {
179 throw new ClassCastException("Only activities can be registered to"
180 + " receive callback from " + ContactSaveService.class.getName());
181 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100182 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800183 }
184
185 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100186 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700187 }
188
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800189 /**
190 * Returns true if the ContactSaveService was started successfully and false if an exception
191 * was thrown and a Toast error message was displayed.
192 */
193 public static boolean startService(Context context, Intent intent, int saveMode) {
194 try {
195 context.startService(intent);
196 } catch (Exception exception) {
197 final int resId;
198 switch (saveMode) {
199 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
200 resId = R.string.contactUnlinkErrorToast;
201 break;
202 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
203 resId = R.string.contactJoinErrorToast;
204 break;
205 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
206 resId = R.string.contactSavedErrorToast;
207 break;
208 default:
209 resId = R.string.contactGenericErrorToast;
210 }
211 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
212 return false;
213 }
214 return true;
215 }
216
217 /**
218 * Utility method that starts service and handles exception.
219 */
220 public static void startService(Context context, Intent intent) {
221 try {
222 context.startService(intent);
223 } catch (Exception exception) {
224 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
225 }
226 }
227
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700228 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800229 public Object getSystemService(String name) {
230 Object service = super.getSystemService(name);
231 if (service != null) {
232 return service;
233 }
234
235 return getApplicationContext().getSystemService(name);
236 }
237
238 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700239 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800240 if (intent == null) {
241 Log.d(TAG, "onHandleIntent: could not handle null intent");
242 return;
243 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700244 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
245 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
246 // TODO: add more specific error string such as "Turn on Contacts
247 // permission to update your contacts"
248 showToast(R.string.contactSavedErrorToast);
249 return;
250 }
251
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700252 // Call an appropriate method. If we're sure it affects how incoming phone calls are
253 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700254 String action = intent.getAction();
255 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
256 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800257 } else if (ACTION_SAVE_CONTACT.equals(action)) {
258 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800259 } else if (ACTION_CREATE_GROUP.equals(action)) {
260 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800261 } else if (ACTION_RENAME_GROUP.equals(action)) {
262 renameGroup(intent);
263 } else if (ACTION_DELETE_GROUP.equals(action)) {
264 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700265 } else if (ACTION_UPDATE_GROUP.equals(action)) {
266 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800267 } else if (ACTION_SET_STARRED.equals(action)) {
268 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800269 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
270 setSuperPrimary(intent);
271 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
272 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800273 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
274 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800275 } else if (ACTION_DELETE_CONTACT.equals(action)) {
276 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700277 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
278 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800279 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
280 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800281 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
282 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700283 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
284 setSendToVoicemail(intent);
285 } else if (ACTION_SET_RINGTONE.equals(action)) {
286 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700287 }
288 }
289
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800290 /**
291 * Creates an intent that can be sent to this service to create a new raw contact
292 * using data presented as a set of ContentValues.
293 */
294 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700295 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700296 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800297 Intent serviceIntent = new Intent(
298 context, ContactSaveService.class);
299 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
300 if (account != null) {
301 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
302 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700303 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800304 }
305 serviceIntent.putParcelableArrayListExtra(
306 ContactSaveService.EXTRA_CONTENT_VALUES, values);
307
308 // Callback intent will be invoked by the service once the new contact is
309 // created. The service will put the URI of the new contact as "data" on
310 // the callback intent.
311 Intent callbackIntent = new Intent(context, callbackActivity);
312 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800313 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
314 return serviceIntent;
315 }
316
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700317 private void createRawContact(Intent intent) {
318 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
319 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700320 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700321 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
322 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
323
324 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
325 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
326 .withValue(RawContacts.ACCOUNT_NAME, accountName)
327 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700328 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700329 .build());
330
331 int size = valueList.size();
332 for (int i = 0; i < size; i++) {
333 ContentValues values = valueList.get(i);
334 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
335 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
336 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
337 .withValues(values)
338 .build());
339 }
340
341 ContentResolver resolver = getContentResolver();
342 ContentProviderResult[] results;
343 try {
344 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
345 } catch (Exception e) {
346 throw new RuntimeException("Failed to store new contact", e);
347 }
348
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700349 Uri rawContactUri = results[0].uri;
350 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
351
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800352 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700353 }
354
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700355 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800356 * Creates an intent that can be sent to this service to create a new raw contact
357 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800358 * This variant is more convenient to use when there is only one photo that can
359 * possibly be updated, as in the Contact Details screen.
360 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
361 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800362 */
Maurice Chu851222a2012-06-21 11:43:08 -0700363 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700364 String saveModeExtraKey, int saveMode, boolean isProfile,
365 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700366 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800367 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700368 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800369 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700370 callbackActivity, callbackAction, bundle,
371 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800372 }
373
374 /**
375 * Creates an intent that can be sent to this service to create a new raw contact
376 * using data presented as a set of ContentValues.
377 * This variant is used when multiple contacts' photos may be updated, as in the
378 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700379 *
Josh Garguse692e012012-01-18 14:53:11 -0800380 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700381 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
382 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800383 */
Maurice Chu851222a2012-06-21 11:43:08 -0700384 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700385 String saveModeExtraKey, int saveMode, boolean isProfile,
386 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700387 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000388 Intent serviceIntent = new Intent(
389 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800390 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
391 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700392 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800393 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
394
Josh Garguse692e012012-01-18 14:53:11 -0800395 if (updatedPhotos != null) {
396 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
397 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800398
Josh Garguse5d3f892012-04-11 11:56:15 -0700399 if (callbackActivity != null) {
400 // Callback intent will be invoked by the service once the contact is
401 // saved. The service will put the URI of the new contact as "data" on
402 // the callback intent.
403 Intent callbackIntent = new Intent(context, callbackActivity);
404 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700405 if (joinContactIdExtraKey != null && joinContactId != null) {
406 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
407 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700408 callbackIntent.setAction(callbackAction);
409 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
410 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800411 return serviceIntent;
412 }
413
414 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700415 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700416 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800417 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800418
Jay Shrauner08099782015-03-25 14:17:11 -0700419 if (state == null) {
420 Log.e(TAG, "Invalid arguments for saveContact request");
421 return;
422 }
423
benny.lin3a4e7a22014-01-08 10:58:08 +0800424 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800425 // Trim any empty fields, and RawContacts, before persisting
426 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700427 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800428
429 Uri lookupUri = null;
430
431 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700432
Josh Garguse692e012012-01-18 14:53:11 -0800433 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800434
Josh Gargusef15c8e2012-01-30 16:42:02 -0800435 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
436 long insertedRawContactId = -1;
437
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800438 // Attempt to persist changes
439 int tries = 0;
440 while (tries++ < PERSIST_TRIES) {
441 try {
442 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800443 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
444
445 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
446
447 for (CPOWrapper cpoWrapper : diffWrapper) {
448 diff.add(cpoWrapper.getOperation());
449 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700450
Katherine Kuana007e442011-07-07 09:25:34 -0700451 if (DEBUG) {
452 Log.v(TAG, "Content Provider Operations:");
453 for (ContentProviderOperation operation : diff) {
454 Log.v(TAG, operation.toString());
455 }
456 }
457
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700458 int numberProcessed = 0;
459 boolean batchFailed = false;
460 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
461 while (numberProcessed < diff.size()) {
462 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
463 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700464 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700465 batchFailed = true;
466 break;
467 } else {
468 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700469 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800470 }
471
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700472 if (batchFailed) {
473 // Retry save
474 continue;
475 }
476
Wenyi Wang67addcc2015-11-23 10:07:48 -0800477 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800478 if (rawContactId == -1) {
479 throw new IllegalStateException("Could not determine RawContact ID after save");
480 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800481 // We don't have to check to see if the value is still -1. If we reach here,
482 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800483 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700484 if (isProfile) {
485 // Since the profile supports local raw contacts, which may have been completely
486 // removed if all information was removed, we need to do a special query to
487 // get the lookup URI for the profile contact (if it still exists).
488 Cursor c = resolver.query(Profile.CONTENT_URI,
489 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
490 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800491 if (c == null) {
492 continue;
493 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700494 try {
Erik162b7e32011-09-20 15:23:55 -0700495 if (c.moveToFirst()) {
496 final long contactId = c.getLong(0);
497 final String lookupKey = c.getString(1);
498 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
499 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700500 } finally {
501 c.close();
502 }
503 } else {
504 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
505 rawContactId);
506 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
507 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800508 if (lookupUri != null) {
509 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
510 }
Josh Garguse692e012012-01-18 14:53:11 -0800511
512 // We can change this back to false later, if we fail to save the contact photo.
513 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800514 break;
515
516 } catch (RemoteException e) {
517 // Something went wrong, bail without success
518 Log.e(TAG, "Problem persisting user edits", e);
519 break;
520
Jay Shrauner57fca182014-01-17 14:20:50 -0800521 } catch (IllegalArgumentException e) {
522 // This is thrown by applyBatch on malformed requests
523 Log.e(TAG, "Problem persisting user edits", e);
524 showToast(R.string.contactSavedErrorToast);
525 break;
526
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800527 } catch (OperationApplicationException e) {
528 // Version consistency failed, re-parent change and try again
529 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
530 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
531 boolean first = true;
532 final int count = state.size();
533 for (int i = 0; i < count; i++) {
534 Long rawContactId = state.getRawContactId(i);
535 if (rawContactId != null && rawContactId != -1) {
536 if (!first) {
537 sb.append(',');
538 }
539 sb.append(rawContactId);
540 first = false;
541 }
542 }
543 sb.append(")");
544
545 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800546 throw new IllegalStateException(
547 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800548 }
549
Maurice Chu851222a2012-06-21 11:43:08 -0700550 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700551 isProfile
552 ? RawContactsEntity.PROFILE_CONTENT_URI
553 : RawContactsEntity.CONTENT_URI,
554 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700555 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700556
557 // Update the new state to use profile URIs if appropriate.
558 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700559 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700560 delta.setProfileQueryUri();
561 }
562 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800563 }
564 }
565
Josh Garguse692e012012-01-18 14:53:11 -0800566 // Now save any updated photos. We do this at the end to ensure that
567 // the ContactProvider already knows about newly-created contacts.
568 if (updatedPhotos != null) {
569 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700570 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800571 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800572
573 // If the raw-contact ID is negative, we are saving a new raw-contact;
574 // replace the bogus ID with the new one that we actually saved the contact at.
575 if (rawContactId < 0) {
576 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800577 }
578
Jay Shrauner511561d2015-04-02 10:35:33 -0700579 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700580 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700581 succeeded = false;
582 }
Josh Garguse692e012012-01-18 14:53:11 -0800583 }
584 }
585
Josh Garguse5d3f892012-04-11 11:56:15 -0700586 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
587 if (callbackIntent != null) {
588 if (succeeded) {
589 // Mark the intent to indicate that the save was successful (even if the lookup URI
590 // is now null). For local contacts or the local profile, it's possible that the
591 // save triggered removal of the contact, so no lookup URI would exist..
592 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
593 }
594 callbackIntent.setData(lookupUri);
595 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800596 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800597 }
598
Josh Garguse692e012012-01-18 14:53:11 -0800599 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700600 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
601 * subsets, adds the returned array to "results".
602 *
603 * @return the size of the array, if not null; -1 when the array is null.
604 */
605 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
606 ContentProviderResult[] results, ContentResolver resolver)
607 throws RemoteException, OperationApplicationException {
608 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
609 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
610 subset.addAll(diff.subList(offset, offset + subsetCount));
611 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
612 .AUTHORITY, subset);
613 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
614 return -1;
615 }
616 for (ContentProviderResult c : subsetResult) {
617 results[offset++] = c;
618 }
619 return subsetResult.length;
620 }
621
622 /**
Josh Garguse692e012012-01-18 14:53:11 -0800623 * Save updated photo for the specified raw-contact.
624 * @return true for success, false for failure
625 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800626 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800627 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800628 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
629 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
630
benny.lin3a4e7a22014-01-08 10:58:08 +0800631 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800632 }
633
Josh Gargusef15c8e2012-01-30 16:42:02 -0800634 /**
635 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
636 */
Maurice Chu851222a2012-06-21 11:43:08 -0700637 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800638 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800639 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800640 long existingRawContactId = state.findRawContactId();
641 if (existingRawContactId != -1) {
642 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800643 }
644
Wenyi Wang67addcc2015-11-23 10:07:48 -0800645 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800646 }
647
648 /**
649 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
650 */
651 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800652 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800653 if (results == null) {
654 return -1;
655 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800656 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800657 final int numResults = results.length;
658 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800659 final CPOWrapper cpoWrapper = diffWrapper.get(i);
660 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
661 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
662 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800663 return ContentUris.parseId(results[i].uri);
664 }
665 }
666 return -1;
667 }
668
669 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700670 * Creates an intent that can be sent to this service to create a new group as
671 * well as add new members at the same time.
672 *
673 * @param context of the application
674 * @param account in which the group should be created
675 * @param label is the name of the group (cannot be null)
676 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
677 * should be added to the group
678 * @param callbackActivity is the activity to send the callback intent to
679 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700680 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700681 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700682 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700683 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800684 Intent serviceIntent = new Intent(context, ContactSaveService.class);
685 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700688 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800689 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700690 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700691
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800692 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700693 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800694 Intent callbackIntent = new Intent(context, callbackActivity);
695 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700696 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800697
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700698 return serviceIntent;
699 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800700
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800701 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700702 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
703 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
704 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
705 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700706 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800707
708 ContentValues values = new ContentValues();
709 values.put(Groups.ACCOUNT_TYPE, accountType);
710 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700711 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800712 values.put(Groups.TITLE, label);
713
Katherine Kuan717e3432011-07-13 17:03:24 -0700714 final ContentResolver resolver = getContentResolver();
715
716 // Create the new group
717 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
718
719 // If there's no URI, then the insertion failed. Abort early because group members can't be
720 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800721 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700722 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800723 return;
724 }
725
Katherine Kuan717e3432011-07-13 17:03:24 -0700726 // Add new group members
727 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
728
729 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
730 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800731 values.clear();
732 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
733 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
734
735 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700736 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700737 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800738 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800739 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800740 }
741
742 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800743 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800744 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700745 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700746 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800747 Intent serviceIntent = new Intent(context, ContactSaveService.class);
748 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
749 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
750 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700751
752 // Callback intent will be invoked by the service once the group is renamed.
753 Intent callbackIntent = new Intent(context, callbackActivity);
754 callbackIntent.setAction(callbackAction);
755 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
756
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800757 return serviceIntent;
758 }
759
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800760 private void renameGroup(Intent intent) {
761 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
762 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
763
764 if (groupId == -1) {
765 Log.e(TAG, "Invalid arguments for renameGroup request");
766 return;
767 }
768
769 ContentValues values = new ContentValues();
770 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700771 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
772 getContentResolver().update(groupUri, values, null, null);
773
774 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
775 callbackIntent.setData(groupUri);
776 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800777 }
778
779 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800780 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800781 */
Walter Jang72f99882016-05-26 09:01:31 -0700782 public static Intent createGroupDeletionIntent(Context context, long groupId,
783 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800784 Intent serviceIntent = new Intent(context, ContactSaveService.class);
785 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800786 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700787
788 // Callback intent will be invoked by the service once the group is updated
789 if (callbackActivity != null && !TextUtils.isEmpty(callbackAction)) {
790 final Intent callbackIntent = new Intent(context, callbackActivity);
791 callbackIntent.setAction(callbackAction);
792 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
793 }
794
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800795 return serviceIntent;
796 }
797
798 private void deleteGroup(Intent intent) {
799 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
800 if (groupId == -1) {
801 Log.e(TAG, "Invalid arguments for deleteGroup request");
802 return;
803 }
804
805 getContentResolver().delete(
806 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
Walter Jang72f99882016-05-26 09:01:31 -0700807
808 final Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
809 if (callbackIntent != null) {
810 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
811 callbackIntent.setData(groupUri);
812 deliverCallback(callbackIntent);
813 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800814 }
815
816 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700817 * Creates an intent that can be sent to this service to rename a group as
818 * well as add and remove members from the group.
819 *
820 * @param context of the application
821 * @param groupId of the group that should be modified
822 * @param newLabel is the updated name of the group (can be null if the name
823 * should not be updated)
824 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
825 * should be added to the group
826 * @param rawContactsToRemove is an array of raw contact IDs for contacts
827 * that should be removed from the group
828 * @param callbackActivity is the activity to send the callback intent to
829 * @param callbackAction is the intent action for the callback intent
830 */
831 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
832 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700833 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700834 Intent serviceIntent = new Intent(context, ContactSaveService.class);
835 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
836 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
837 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
838 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
839 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
840 rawContactsToRemove);
841
842 // Callback intent will be invoked by the service once the group is updated
843 Intent callbackIntent = new Intent(context, callbackActivity);
844 callbackIntent.setAction(callbackAction);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
846
847 return serviceIntent;
848 }
849
850 private void updateGroup(Intent intent) {
851 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
852 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
853 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
854 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
855
856 if (groupId == -1) {
857 Log.e(TAG, "Invalid arguments for updateGroup request");
858 return;
859 }
860
861 final ContentResolver resolver = getContentResolver();
862 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
863
864 // Update group name if necessary
865 if (label != null) {
866 ContentValues values = new ContentValues();
867 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700868 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700869 }
870
Katherine Kuan717e3432011-07-13 17:03:24 -0700871 // Add and remove members if necessary
872 addMembersToGroup(resolver, rawContactsToAdd, groupId);
873 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
874
875 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
876 callbackIntent.setData(groupUri);
877 deliverCallback(callbackIntent);
878 }
879
Daniel Lehmann18958a22012-02-28 17:45:25 -0800880 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700881 long groupId) {
882 if (rawContactsToAdd == null) {
883 return;
884 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700885 for (long rawContactId : rawContactsToAdd) {
886 try {
887 final ArrayList<ContentProviderOperation> rawContactOperations =
888 new ArrayList<ContentProviderOperation>();
889
890 // Build an assert operation to ensure the contact is not already in the group
891 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
892 .newAssertQuery(Data.CONTENT_URI);
893 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
894 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
895 new String[] { String.valueOf(rawContactId),
896 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
897 assertBuilder.withExpectedCount(0);
898 rawContactOperations.add(assertBuilder.build());
899
900 // Build an insert operation to add the contact to the group
901 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
902 .newInsert(Data.CONTENT_URI);
903 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
904 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
905 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
906 rawContactOperations.add(insertBuilder.build());
907
908 if (DEBUG) {
909 for (ContentProviderOperation operation : rawContactOperations) {
910 Log.v(TAG, operation.toString());
911 }
912 }
913
914 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700915 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800916 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700917 }
918 } catch (RemoteException e) {
919 // Something went wrong, bail without success
920 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
921 String.valueOf(rawContactId), e);
922 } catch (OperationApplicationException e) {
923 // The assert could have failed because the contact is already in the group,
924 // just continue to the next contact
925 Log.w(TAG, "Assert failed in adding raw contact ID " +
926 String.valueOf(rawContactId) + ". Already exists in group " +
927 String.valueOf(groupId), e);
928 }
929 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700930 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700931
Daniel Lehmann18958a22012-02-28 17:45:25 -0800932 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700933 long groupId) {
934 if (rawContactsToRemove == null) {
935 return;
936 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700937 for (long rawContactId : rawContactsToRemove) {
938 // Apply the delete operation on the data row for the given raw contact's
939 // membership in the given group. If no contact matches the provided selection, then
940 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800941 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700942 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
943 new String[] { String.valueOf(rawContactId),
944 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
945 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700946 }
947
948 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800949 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800950 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800951 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
952 Intent serviceIntent = new Intent(context, ContactSaveService.class);
953 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
954 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
955 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
956
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800957 return serviceIntent;
958 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800959
960 private void setStarred(Intent intent) {
961 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
962 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
963 if (contactUri == null) {
964 Log.e(TAG, "Invalid arguments for setStarred request");
965 return;
966 }
967
968 final ContentValues values = new ContentValues(1);
969 values.put(Contacts.STARRED, value);
970 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700971
972 // Undemote the contact if necessary
973 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
974 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800975 if (c == null) {
976 return;
977 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700978 try {
979 if (c.moveToFirst()) {
980 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700981
982 // Don't bother undemoting if this contact is the user's profile.
983 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800984 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700985 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700986 }
987 } finally {
988 c.close();
989 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800990 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800991
992 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700993 * Creates an intent that can be sent to this service to set the redirect to voicemail.
994 */
995 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
996 boolean value) {
997 Intent serviceIntent = new Intent(context, ContactSaveService.class);
998 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
999 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1000 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1001
1002 return serviceIntent;
1003 }
1004
1005 private void setSendToVoicemail(Intent intent) {
1006 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1007 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1008 if (contactUri == null) {
1009 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1010 return;
1011 }
1012
1013 final ContentValues values = new ContentValues(1);
1014 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1015 getContentResolver().update(contactUri, values, null, null);
1016 }
1017
1018 /**
1019 * Creates an intent that can be sent to this service to save the contact's ringtone.
1020 */
1021 public static Intent createSetRingtone(Context context, Uri contactUri,
1022 String value) {
1023 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1024 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1025 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1026 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1027
1028 return serviceIntent;
1029 }
1030
1031 private void setRingtone(Intent intent) {
1032 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1033 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1034 if (contactUri == null) {
1035 Log.e(TAG, "Invalid arguments for setRingtone");
1036 return;
1037 }
1038 ContentValues values = new ContentValues(1);
1039 values.put(Contacts.CUSTOM_RINGTONE, value);
1040 getContentResolver().update(contactUri, values, null, null);
1041 }
1042
1043 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001044 * Creates an intent that sets the selected data item as super primary (default)
1045 */
1046 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1047 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1048 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1049 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1050 return serviceIntent;
1051 }
1052
1053 private void setSuperPrimary(Intent intent) {
1054 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1055 if (dataId == -1) {
1056 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1057 return;
1058 }
1059
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001060 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001061 }
1062
1063 /**
1064 * Creates an intent that clears the primary flag of all data items that belong to the same
1065 * raw_contact as the given data item. Will only clear, if the data item was primary before
1066 * this call
1067 */
1068 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1069 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1070 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1071 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1072 return serviceIntent;
1073 }
1074
1075 private void clearPrimary(Intent intent) {
1076 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1077 if (dataId == -1) {
1078 Log.e(TAG, "Invalid arguments for clearPrimary request");
1079 return;
1080 }
1081
1082 // Update the primary values in the data record.
1083 ContentValues values = new ContentValues(1);
1084 values.put(Data.IS_SUPER_PRIMARY, 0);
1085 values.put(Data.IS_PRIMARY, 0);
1086
1087 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1088 values, null, null);
1089 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001090
1091 /**
1092 * Creates an intent that can be sent to this service to delete a contact.
1093 */
1094 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1095 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1096 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1097 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1098 return serviceIntent;
1099 }
1100
Brian Attwelld2962a32015-03-02 14:48:50 -08001101 /**
1102 * Creates an intent that can be sent to this service to delete multiple contacts.
1103 */
1104 public static Intent createDeleteMultipleContactsIntent(Context context,
1105 long[] contactIds) {
1106 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1107 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1108 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1109 return serviceIntent;
1110 }
1111
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001112 private void deleteContact(Intent intent) {
1113 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1114 if (contactUri == null) {
1115 Log.e(TAG, "Invalid arguments for deleteContact request");
1116 return;
1117 }
1118
1119 getContentResolver().delete(contactUri, null, null);
1120 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001121
Brian Attwelld2962a32015-03-02 14:48:50 -08001122 private void deleteMultipleContacts(Intent intent) {
1123 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1124 if (contactIds == null) {
1125 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1126 return;
1127 }
1128 for (long contactId : contactIds) {
1129 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1130 getContentResolver().delete(contactUri, null, null);
1131 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001132 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1133 .contacts_deleted_toast, contactIds.length);
1134 mMainHandler.post(new Runnable() {
1135 @Override
1136 public void run() {
1137 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1138 .show();
1139 }
1140 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001141 }
1142
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001143 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001144 * Creates an intent that can be sent to this service to split a contact into it's constituent
1145 * pieces.
1146 */
1147 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1148 ResultReceiver receiver) {
1149 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1150 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1151 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1152 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1153 return serviceIntent;
1154 }
1155
1156 private void splitContact(Intent intent) {
1157 final long rawContactIds[][] = (long[][]) intent
1158 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001159 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001160 if (rawContactIds == null) {
1161 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001162 if (receiver != null) {
1163 receiver.send(BAD_ARGUMENTS, new Bundle());
1164 }
Gary Mai7efa9942016-05-12 11:26:49 -07001165 return;
1166 }
1167 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1168 final ContentResolver resolver = getContentResolver();
1169 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001170 for (int i = 0; i < rawContactIds.length; i++) {
1171 for (int j = 0; j < rawContactIds.length; j++) {
1172 if (i != j) {
1173 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1174 if (receiver != null) {
1175 receiver.send(CP2_ERROR, new Bundle());
1176 return;
1177 }
1178 }
1179 }
1180 }
1181 }
1182 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1183 if (receiver != null) {
1184 receiver.send(CP2_ERROR, new Bundle());
1185 }
1186 return;
1187 }
1188 if (receiver != null) {
1189 receiver.send(CONTACTS_SPLIT, new Bundle());
1190 } else {
1191 showToast(R.string.contactUnlinkedToast);
1192 }
1193 }
1194
1195 /**
1196 * Adds insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
1197 * and {@param rawContactIds2} to {@param operations}.
1198 * @return false if an error occurred, true otherwise.
1199 */
1200 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1201 long[] rawContactIds1, long[] rawContactIds2) {
1202 if (rawContactIds1 == null || rawContactIds2 == null) {
1203 Log.e(TAG, "Invalid arguments for splitContact request");
1204 return false;
1205 }
1206 // For each pair of raw contacts, insert an aggregation exception
1207 final ContentResolver resolver = getContentResolver();
1208 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1209 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1210 for (int i = 0; i < rawContactIds1.length; i++) {
1211 for (int j = 0; j < rawContactIds2.length; j++) {
1212 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1213 // Before we get to 500 we need to flush the operations list
1214 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1215 if (!applyOperations(resolver, operations)) {
1216 return false;
1217 }
1218 operations.clear();
1219 }
1220 }
1221 }
1222 return true;
1223 }
1224
1225 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001226 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001227 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001228 */
1229 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001230 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001231 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1232 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1233 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1234 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001235
1236 // Callback intent will be invoked by the service once the contacts are joined.
1237 Intent callbackIntent = new Intent(context, callbackActivity);
1238 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001239 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1240
1241 return serviceIntent;
1242 }
1243
Brian Attwelld3946ca2015-03-03 11:13:49 -08001244 /**
1245 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1246 * No special attention is paid to where the resulting contact's name is taken from.
1247 */
Gary Mai7efa9942016-05-12 11:26:49 -07001248 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1249 ResultReceiver receiver) {
1250 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001251 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1252 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001253 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001254 return serviceIntent;
1255 }
1256
Gary Mai7efa9942016-05-12 11:26:49 -07001257 /**
1258 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1259 * No special attention is paid to where the resulting contact's name is taken from.
1260 */
1261 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1262 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1263 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001264
1265 private interface JoinContactQuery {
1266 String[] PROJECTION = {
1267 RawContacts._ID,
1268 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001269 RawContacts.DISPLAY_NAME_SOURCE,
1270 };
1271
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001272 int _ID = 0;
1273 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001274 int DISPLAY_NAME_SOURCE = 2;
1275 }
1276
1277 private interface ContactEntityQuery {
1278 String[] PROJECTION = {
1279 Contacts.Entity.DATA_ID,
1280 Contacts.Entity.CONTACT_ID,
1281 Contacts.Entity.IS_SUPER_PRIMARY,
1282 };
1283 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1284 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1285 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1286 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1287
1288 int DATA_ID = 0;
1289 int CONTACT_ID = 1;
1290 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001291 }
1292
Brian Attwelld3946ca2015-03-03 11:13:49 -08001293 private void joinSeveralContacts(Intent intent) {
1294 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Gary Mai7efa9942016-05-12 11:26:49 -07001295 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001296
Brian Attwelld3946ca2015-03-03 11:13:49 -08001297 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001298 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1299 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001300 if (rawContactIds == null) {
1301 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001302 if (receiver != null) {
1303 receiver.send(BAD_ARGUMENTS, new Bundle());
1304 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001305 return;
1306 }
1307
Brian Attwelld3946ca2015-03-03 11:13:49 -08001308 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001309 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001310 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1311 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1312 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001313 for (int i = 0; i < rawContactIds.length; i++) {
1314 for (int j = 0; j < rawContactIds.length; j++) {
1315 if (i != j) {
1316 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1317 }
Walter Jang0653de32015-07-24 12:12:40 -07001318 // Before we get to 500 we need to flush the operations list
1319 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001320 if (!applyOperations(resolver, operations)) {
1321 if (receiver != null) {
1322 receiver.send(CP2_ERROR, new Bundle());
1323 }
Walter Jang0653de32015-07-24 12:12:40 -07001324 return;
1325 }
1326 operations.clear();
1327 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001328 }
1329 }
Gary Mai7efa9942016-05-12 11:26:49 -07001330 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1331 if (receiver != null) {
1332 receiver.send(CP2_ERROR, new Bundle());
1333 }
Walter Jang0653de32015-07-24 12:12:40 -07001334 return;
1335 }
Gary Mai7efa9942016-05-12 11:26:49 -07001336 if (receiver != null) {
1337 final Bundle result = new Bundle();
1338 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1339 receiver.send(CONTACTS_LINKED, result);
1340 } else {
1341 showToast(R.string.contactsJoinedMessage);
1342 }
Walter Jang0653de32015-07-24 12:12:40 -07001343 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001344
Walter Jang0653de32015-07-24 12:12:40 -07001345 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001346 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001347 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001348 try {
1349 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001350 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001351 } catch (RemoteException | OperationApplicationException e) {
1352 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1353 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001354 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001355 }
1356 }
1357
Brian Attwelld3946ca2015-03-03 11:13:49 -08001358 private void joinContacts(Intent intent) {
1359 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1360 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001361
1362 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001363 // in the join UIs.
1364 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1365 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001366 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001367 return;
1368 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001369
Brian Attwell548f5c62015-01-27 17:46:46 -08001370 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001371
1372 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001373 for (int i = 0; i < rawContactIds.length; i++) {
1374 for (int j = 0; j < rawContactIds.length; j++) {
1375 if (i != j) {
1376 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1377 }
1378 }
1379 }
1380
Brian Attwelld3946ca2015-03-03 11:13:49 -08001381 final ContentResolver resolver = getContentResolver();
1382
Brian Attwell548f5c62015-01-27 17:46:46 -08001383 // Use the name for contactId1 as the name for the newly aggregated contact.
1384 final Uri contactId1Uri = ContentUris.withAppendedId(
1385 Contacts.CONTENT_URI, contactId1);
1386 final Uri entityUri = Uri.withAppendedPath(
1387 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1388 Cursor c = resolver.query(entityUri,
1389 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1390 if (c == null) {
1391 Log.e(TAG, "Unable to open Contacts DB cursor");
1392 showToast(R.string.contactSavedErrorToast);
1393 return;
1394 }
1395 long dataIdToAddSuperPrimary = -1;
1396 try {
1397 if (c.moveToFirst()) {
1398 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1399 }
1400 } finally {
1401 c.close();
1402 }
1403
1404 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1405 // display name does not change as a result of the join.
1406 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001407 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001408 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1409 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1410 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001411 operations.add(builder.build());
1412 }
1413
1414 boolean success = false;
1415 // Apply all aggregation exceptions as one batch
1416 try {
1417 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001418 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001419 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001420 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001421 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001422 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001423 }
1424
1425 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1426 if (success) {
1427 Uri uri = RawContacts.getContactLookupUri(resolver,
1428 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1429 callbackIntent.setData(uri);
1430 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001431 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001432 }
1433
Gary Mai7efa9942016-05-12 11:26:49 -07001434 /**
1435 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1436 * array of the return value holds an array of raw contact ids for one contactId.
1437 * @param contactIds
1438 * @return
1439 */
1440 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1441 final long[][] rawContactIds = new long[contactIds.length][];
1442 for (int i = 0; i < contactIds.length; i++) {
1443 rawContactIds[i] = getRawContactIds(contactIds[i]);
1444 }
1445 return rawContactIds;
1446 }
1447
1448 /**
1449 * Gets the raw contact ids associated with {@param contactId}.
1450 * @param contactId
1451 * @return Array of raw contact ids.
1452 */
1453 private long[] getRawContactIds(long contactId) {
1454 final ContentResolver resolver = getContentResolver();
1455 long rawContactIds[];
1456
1457 final StringBuilder queryBuilder = new StringBuilder();
1458 queryBuilder.append(RawContacts.CONTACT_ID)
1459 .append("=")
1460 .append(String.valueOf(contactId));
1461
1462 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1463 JoinContactQuery.PROJECTION,
1464 queryBuilder.toString(),
1465 null, null);
1466 if (c == null) {
1467 Log.e(TAG, "Unable to open Contacts DB cursor");
1468 return null;
1469 }
1470 try {
1471 rawContactIds = new long[c.getCount()];
1472 for (int i = 0; i < rawContactIds.length; i++) {
1473 c.moveToPosition(i);
1474 final long rawContactId = c.getLong(JoinContactQuery._ID);
1475 rawContactIds[i] = rawContactId;
1476 }
1477 } finally {
1478 c.close();
1479 }
1480 return rawContactIds;
1481 }
1482
Brian Attwelld3946ca2015-03-03 11:13:49 -08001483 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1484 if (contactIds == null) {
1485 return null;
1486 }
1487
Brian Attwell548f5c62015-01-27 17:46:46 -08001488 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001489
1490 final StringBuilder queryBuilder = new StringBuilder();
1491 final String stringContactIds[] = new String[contactIds.length];
1492 for (int i = 0; i < contactIds.length; i++) {
1493 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1494 stringContactIds[i] = String.valueOf(contactIds[i]);
1495 if (contactIds[i] == -1) {
1496 return null;
1497 }
1498 if (i == contactIds.length -1) {
1499 break;
1500 }
1501 queryBuilder.append(" OR ");
1502 }
1503
Brian Attwell548f5c62015-01-27 17:46:46 -08001504 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1505 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001506 queryBuilder.toString(),
1507 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001508 if (c == null) {
1509 Log.e(TAG, "Unable to open Contacts DB cursor");
1510 showToast(R.string.contactSavedErrorToast);
1511 return null;
1512 }
Gary Mai7efa9942016-05-12 11:26:49 -07001513 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001514 try {
1515 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001516 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001517 return null;
1518 }
1519 rawContactIds = new long[c.getCount()];
1520 for (int i = 0; i < rawContactIds.length; i++) {
1521 c.moveToPosition(i);
1522 long rawContactId = c.getLong(JoinContactQuery._ID);
1523 rawContactIds[i] = rawContactId;
1524 }
1525 } finally {
1526 c.close();
1527 }
1528 return rawContactIds;
1529 }
1530
Brian Attwelld3946ca2015-03-03 11:13:49 -08001531 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1532 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1533 }
1534
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001535 /**
1536 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1537 */
1538 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1539 long rawContactId1, long rawContactId2) {
1540 Builder builder =
1541 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1542 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1543 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1544 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1545 operations.add(builder.build());
1546 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001547
1548 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001549 * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation.
1550 */
1551 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1552 long rawContactId1, long rawContactId2) {
1553 final Builder builder =
1554 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1555 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
1556 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1557 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1558 operations.add(builder.build());
1559 }
1560
1561 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001562 * Shows a toast on the UI thread.
1563 */
1564 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001565 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001566
1567 @Override
1568 public void run() {
1569 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1570 }
1571 });
1572 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001573
1574 private void deliverCallback(final Intent callbackIntent) {
1575 mMainHandler.post(new Runnable() {
1576
1577 @Override
1578 public void run() {
1579 deliverCallbackOnUiThread(callbackIntent);
1580 }
1581 });
1582 }
1583
1584 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1585 // TODO: this assumes that if there are multiple instances of the same
1586 // activity registered, the last one registered is the one waiting for
1587 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001588 for (Listener listener : sListeners) {
1589 if (callbackIntent.getComponent().equals(
1590 ((Activity) listener).getIntent().getComponent())) {
1591 listener.onServiceCompleted(callbackIntent);
1592 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001593 }
1594 }
1595 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001596}