1 /* 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 17 package com.android.contacts; 18 19 import static android.Manifest.permission.WRITE_CONTACTS; 20 import android.app.Activity; 21 import android.app.IntentService; 22 import android.content.ContentProviderOperation; 23 import android.content.ContentProviderOperation.Builder; 24 import android.content.ContentProviderResult; 25 import android.content.ContentResolver; 26 import android.content.ContentUris; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.OperationApplicationException; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.Parcelable; 37 import android.os.RemoteException; 38 import android.provider.ContactsContract; 39 import android.provider.ContactsContract.AggregationExceptions; 40 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 41 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 42 import android.provider.ContactsContract.Contacts; 43 import android.provider.ContactsContract.Data; 44 import android.provider.ContactsContract.Groups; 45 import android.provider.ContactsContract.Profile; 46 import android.provider.ContactsContract.RawContacts; 47 import android.provider.ContactsContract.RawContactsEntity; 48 import android.util.Log; 49 import android.widget.Toast; 50 51 import com.android.contacts.activities.ContactEditorBaseActivity; 52 import com.android.contacts.common.compat.CompatUtils; 53 import com.android.contacts.common.database.ContactUpdateUtils; 54 import com.android.contacts.common.model.AccountTypeManager; 55 import com.android.contacts.common.model.CPOWrapper; 56 import com.android.contacts.common.model.RawContactDelta; 57 import com.android.contacts.common.model.RawContactDeltaList; 58 import com.android.contacts.common.model.RawContactModifier; 59 import com.android.contacts.common.model.account.AccountWithDataSet; 60 import com.android.contacts.common.util.PermissionsUtil; 61 import com.android.contacts.compat.PinnedPositionsCompat; 62 import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor.SaveMode; 63 import com.android.contacts.util.ContactPhotoUtils; 64 65 import com.google.common.collect.Lists; 66 import com.google.common.collect.Sets; 67 68 import java.util.ArrayList; 69 import java.util.HashSet; 70 import java.util.List; 71 import java.util.concurrent.CopyOnWriteArrayList; 72 73 /** 74 * A service responsible for saving changes to the content provider. 75 */ 76 public class ContactSaveService extends IntentService { 77 private static final String TAG = "ContactSaveService"; 78 79 /** Set to true in order to view logs on content provider operations */ 80 private static final boolean DEBUG = false; 81 82 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact"; 83 84 public static final String EXTRA_ACCOUNT_NAME = "accountName"; 85 public static final String EXTRA_ACCOUNT_TYPE = "accountType"; 86 public static final String EXTRA_DATA_SET = "dataSet"; 87 public static final String EXTRA_CONTENT_VALUES = "contentValues"; 88 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent"; 89 90 public static final String ACTION_SAVE_CONTACT = "saveContact"; 91 public static final String EXTRA_CONTACT_STATE = "state"; 92 public static final String EXTRA_SAVE_MODE = "saveMode"; 93 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile"; 94 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded"; 95 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos"; 96 97 public static final String ACTION_CREATE_GROUP = "createGroup"; 98 public static final String ACTION_RENAME_GROUP = "renameGroup"; 99 public static final String ACTION_DELETE_GROUP = "deleteGroup"; 100 public static final String ACTION_UPDATE_GROUP = "updateGroup"; 101 public static final String EXTRA_GROUP_ID = "groupId"; 102 public static final String EXTRA_GROUP_LABEL = "groupLabel"; 103 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd"; 104 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove"; 105 106 public static final String ACTION_SET_STARRED = "setStarred"; 107 public static final String ACTION_DELETE_CONTACT = "delete"; 108 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts"; 109 public static final String EXTRA_CONTACT_URI = "contactUri"; 110 public static final String EXTRA_CONTACT_IDS = "contactIds"; 111 public static final String EXTRA_STARRED_FLAG = "starred"; 112 113 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary"; 114 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary"; 115 public static final String EXTRA_DATA_ID = "dataId"; 116 117 public static final String ACTION_JOIN_CONTACTS = "joinContacts"; 118 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts"; 119 public static final String EXTRA_CONTACT_ID1 = "contactId1"; 120 public static final String EXTRA_CONTACT_ID2 = "contactId2"; 121 122 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail"; 123 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag"; 124 125 public static final String ACTION_SET_RINGTONE = "setRingtone"; 126 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone"; 127 128 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet( 129 Data.MIMETYPE, 130 Data.IS_PRIMARY, 131 Data.DATA1, 132 Data.DATA2, 133 Data.DATA3, 134 Data.DATA4, 135 Data.DATA5, 136 Data.DATA6, 137 Data.DATA7, 138 Data.DATA8, 139 Data.DATA9, 140 Data.DATA10, 141 Data.DATA11, 142 Data.DATA12, 143 Data.DATA13, 144 Data.DATA14, 145 Data.DATA15 146 ); 147 148 private static final int PERSIST_TRIES = 3; 149 150 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499; 151 152 public interface Listener { 153 public void onServiceCompleted(Intent callbackIntent); 154 } 155 156 private static final CopyOnWriteArrayList<Listener> sListeners = 157 new CopyOnWriteArrayList<Listener>(); 158 159 private Handler mMainHandler; 160 161 public ContactSaveService() { 162 super(TAG); 163 setIntentRedelivery(true); 164 mMainHandler = new Handler(Looper.getMainLooper()); 165 } 166 167 public static void registerListener(Listener listener) { 168 if (!(listener instanceof Activity)) { 169 throw new ClassCastException("Only activities can be registered to" 170 + " receive callback from " + ContactSaveService.class.getName()); 171 } 172 sListeners.add(0, listener); 173 } 174 175 public static void unregisterListener(Listener listener) { 176 sListeners.remove(listener); 177 } 178 179 /** 180 * Returns true if the ContactSaveService was started successfully and false if an exception 181 * was thrown and a Toast error message was displayed. 182 */ 183 public static boolean startService(Context context, Intent intent, int saveMode) { 184 try { 185 context.startService(intent); 186 } catch (Exception exception) { 187 final int resId; 188 switch (saveMode) { 189 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT: 190 resId = R.string.contactUnlinkErrorToast; 191 break; 192 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD: 193 resId = R.string.contactJoinErrorToast; 194 break; 195 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE: 196 resId = R.string.contactSavedErrorToast; 197 break; 198 default: 199 resId = R.string.contactGenericErrorToast; 200 } 201 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show(); 202 return false; 203 } 204 return true; 205 } 206 207 /** 208 * Utility method that starts service and handles exception. 209 */ 210 public static void startService(Context context, Intent intent) { 211 try { 212 context.startService(intent); 213 } catch (Exception exception) { 214 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show(); 215 } 216 } 217 218 @Override 219 public Object getSystemService(String name) { 220 Object service = super.getSystemService(name); 221 if (service != null) { 222 return service; 223 } 224 225 return getApplicationContext().getSystemService(name); 226 } 227 228 @Override 229 protected void onHandleIntent(Intent intent) { 230 if (intent == null) { 231 Log.d(TAG, "onHandleIntent: could not handle null intent"); 232 return; 233 } 234 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) { 235 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2"); 236 // TODO: add more specific error string such as "Turn on Contacts 237 // permission to update your contacts" 238 showToast(R.string.contactSavedErrorToast); 239 return; 240 } 241 242 // Call an appropriate method. If we're sure it affects how incoming phone calls are 243 // handled, then notify the fact to in-call screen. 244 String action = intent.getAction(); 245 if (ACTION_NEW_RAW_CONTACT.equals(action)) { 246 createRawContact(intent); 247 } else if (ACTION_SAVE_CONTACT.equals(action)) { 248 saveContact(intent); 249 } else if (ACTION_CREATE_GROUP.equals(action)) { 250 createGroup(intent); 251 } else if (ACTION_RENAME_GROUP.equals(action)) { 252 renameGroup(intent); 253 } else if (ACTION_DELETE_GROUP.equals(action)) { 254 deleteGroup(intent); 255 } else if (ACTION_UPDATE_GROUP.equals(action)) { 256 updateGroup(intent); 257 } else if (ACTION_SET_STARRED.equals(action)) { 258 setStarred(intent); 259 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) { 260 setSuperPrimary(intent); 261 } else if (ACTION_CLEAR_PRIMARY.equals(action)) { 262 clearPrimary(intent); 263 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) { 264 deleteMultipleContacts(intent); 265 } else if (ACTION_DELETE_CONTACT.equals(action)) { 266 deleteContact(intent); 267 } else if (ACTION_JOIN_CONTACTS.equals(action)) { 268 joinContacts(intent); 269 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) { 270 joinSeveralContacts(intent); 271 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) { 272 setSendToVoicemail(intent); 273 } else if (ACTION_SET_RINGTONE.equals(action)) { 274 setRingtone(intent); 275 } 276 } 277 278 /** 279 * Creates an intent that can be sent to this service to create a new raw contact 280 * using data presented as a set of ContentValues. 281 */ 282 public static Intent createNewRawContactIntent(Context context, 283 ArrayList<ContentValues> values, AccountWithDataSet account, 284 Class<? extends Activity> callbackActivity, String callbackAction) { 285 Intent serviceIntent = new Intent( 286 context, ContactSaveService.class); 287 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT); 288 if (account != null) { 289 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name); 290 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type); 291 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet); 292 } 293 serviceIntent.putParcelableArrayListExtra( 294 ContactSaveService.EXTRA_CONTENT_VALUES, values); 295 296 // Callback intent will be invoked by the service once the new contact is 297 // created. The service will put the URI of the new contact as "data" on 298 // the callback intent. 299 Intent callbackIntent = new Intent(context, callbackActivity); 300 callbackIntent.setAction(callbackAction); 301 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 302 return serviceIntent; 303 } 304 305 private void createRawContact(Intent intent) { 306 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); 307 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); 308 String dataSet = intent.getStringExtra(EXTRA_DATA_SET); 309 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES); 310 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 311 312 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 313 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) 314 .withValue(RawContacts.ACCOUNT_NAME, accountName) 315 .withValue(RawContacts.ACCOUNT_TYPE, accountType) 316 .withValue(RawContacts.DATA_SET, dataSet) 317 .build()); 318 319 int size = valueList.size(); 320 for (int i = 0; i < size; i++) { 321 ContentValues values = valueList.get(i); 322 values.keySet().retainAll(ALLOWED_DATA_COLUMNS); 323 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI) 324 .withValueBackReference(Data.RAW_CONTACT_ID, 0) 325 .withValues(values) 326 .build()); 327 } 328 329 ContentResolver resolver = getContentResolver(); 330 ContentProviderResult[] results; 331 try { 332 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations); 333 } catch (Exception e) { 334 throw new RuntimeException("Failed to store new contact", e); 335 } 336 337 Uri rawContactUri = results[0].uri; 338 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri)); 339 340 deliverCallback(callbackIntent); 341 } 342 343 /** 344 * Creates an intent that can be sent to this service to create a new raw contact 345 * using data presented as a set of ContentValues. 346 * This variant is more convenient to use when there is only one photo that can 347 * possibly be updated, as in the Contact Details screen. 348 * @param rawContactId identifies a writable raw-contact whose photo is to be updated. 349 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo. 350 */ 351 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state, 352 String saveModeExtraKey, int saveMode, boolean isProfile, 353 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId, 354 Uri updatedPhotoPath) { 355 Bundle bundle = new Bundle(); 356 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath); 357 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile, 358 callbackActivity, callbackAction, bundle, 359 /* joinContactIdExtraKey */ null, /* joinContactId */ null); 360 } 361 362 /** 363 * Creates an intent that can be sent to this service to create a new raw contact 364 * using data presented as a set of ContentValues. 365 * This variant is used when multiple contacts' photos may be updated, as in the 366 * Contact Editor. 367 * 368 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo. 369 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent. 370 * @param joinContactId the raw contact ID to join to the contact after doing the save. 371 */ 372 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state, 373 String saveModeExtraKey, int saveMode, boolean isProfile, 374 Class<? extends Activity> callbackActivity, String callbackAction, 375 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) { 376 Intent serviceIntent = new Intent( 377 context, ContactSaveService.class); 378 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT); 379 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state); 380 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile); 381 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode); 382 383 if (updatedPhotos != null) { 384 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos); 385 } 386 387 if (callbackActivity != null) { 388 // Callback intent will be invoked by the service once the contact is 389 // saved. The service will put the URI of the new contact as "data" on 390 // the callback intent. 391 Intent callbackIntent = new Intent(context, callbackActivity); 392 callbackIntent.putExtra(saveModeExtraKey, saveMode); 393 if (joinContactIdExtraKey != null && joinContactId != null) { 394 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId); 395 } 396 callbackIntent.setAction(callbackAction); 397 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 398 } 399 return serviceIntent; 400 } 401 402 private void saveContact(Intent intent) { 403 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE); 404 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false); 405 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS); 406 407 if (state == null) { 408 Log.e(TAG, "Invalid arguments for saveContact request"); 409 return; 410 } 411 412 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1); 413 // Trim any empty fields, and RawContacts, before persisting 414 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this); 415 RawContactModifier.trimEmpty(state, accountTypes); 416 417 Uri lookupUri = null; 418 419 final ContentResolver resolver = getContentResolver(); 420 421 boolean succeeded = false; 422 423 // Keep track of the id of a newly raw-contact (if any... there can be at most one). 424 long insertedRawContactId = -1; 425 426 // Attempt to persist changes 427 int tries = 0; 428 while (tries++ < PERSIST_TRIES) { 429 try { 430 // Build operations and try applying 431 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper(); 432 433 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList(); 434 435 for (CPOWrapper cpoWrapper : diffWrapper) { 436 diff.add(cpoWrapper.getOperation()); 437 } 438 439 if (DEBUG) { 440 Log.v(TAG, "Content Provider Operations:"); 441 for (ContentProviderOperation operation : diff) { 442 Log.v(TAG, operation.toString()); 443 } 444 } 445 446 int numberProcessed = 0; 447 boolean batchFailed = false; 448 final ContentProviderResult[] results = new ContentProviderResult[diff.size()]; 449 while (numberProcessed < diff.size()) { 450 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver); 451 if (subsetCount == -1) { 452 Log.w(TAG, "Resolver.applyBatch failed in saveContacts"); 453 batchFailed = true; 454 break; 455 } else { 456 numberProcessed += subsetCount; 457 } 458 } 459 460 if (batchFailed) { 461 // Retry save 462 continue; 463 } 464 465 final long rawContactId = getRawContactId(state, diffWrapper, results); 466 if (rawContactId == -1) { 467 throw new IllegalStateException("Could not determine RawContact ID after save"); 468 } 469 // We don't have to check to see if the value is still -1. If we reach here, 470 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus. 471 insertedRawContactId = getInsertedRawContactId(diffWrapper, results); 472 if (isProfile) { 473 // Since the profile supports local raw contacts, which may have been completely 474 // removed if all information was removed, we need to do a special query to 475 // get the lookup URI for the profile contact (if it still exists). 476 Cursor c = resolver.query(Profile.CONTENT_URI, 477 new String[] {Contacts._ID, Contacts.LOOKUP_KEY}, 478 null, null, null); 479 if (c == null) { 480 continue; 481 } 482 try { 483 if (c.moveToFirst()) { 484 final long contactId = c.getLong(0); 485 final String lookupKey = c.getString(1); 486 lookupUri = Contacts.getLookupUri(contactId, lookupKey); 487 } 488 } finally { 489 c.close(); 490 } 491 } else { 492 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, 493 rawContactId); 494 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri); 495 } 496 if (lookupUri != null) { 497 Log.v(TAG, "Saved contact. New URI: " + lookupUri); 498 } 499 500 // We can change this back to false later, if we fail to save the contact photo. 501 succeeded = true; 502 break; 503 504 } catch (RemoteException e) { 505 // Something went wrong, bail without success 506 Log.e(TAG, "Problem persisting user edits", e); 507 break; 508 509 } catch (IllegalArgumentException e) { 510 // This is thrown by applyBatch on malformed requests 511 Log.e(TAG, "Problem persisting user edits", e); 512 showToast(R.string.contactSavedErrorToast); 513 break; 514 515 } catch (OperationApplicationException e) { 516 // Version consistency failed, re-parent change and try again 517 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString()); 518 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN("); 519 boolean first = true; 520 final int count = state.size(); 521 for (int i = 0; i < count; i++) { 522 Long rawContactId = state.getRawContactId(i); 523 if (rawContactId != null && rawContactId != -1) { 524 if (!first) { 525 sb.append(','); 526 } 527 sb.append(rawContactId); 528 first = false; 529 } 530 } 531 sb.append(")"); 532 533 if (first) { 534 throw new IllegalStateException( 535 "Version consistency failed for a new contact", e); 536 } 537 538 final RawContactDeltaList newState = RawContactDeltaList.fromQuery( 539 isProfile 540 ? RawContactsEntity.PROFILE_CONTENT_URI 541 : RawContactsEntity.CONTENT_URI, 542 resolver, sb.toString(), null, null); 543 state = RawContactDeltaList.mergeAfter(newState, state); 544 545 // Update the new state to use profile URIs if appropriate. 546 if (isProfile) { 547 for (RawContactDelta delta : state) { 548 delta.setProfileQueryUri(); 549 } 550 } 551 } 552 } 553 554 // Now save any updated photos. We do this at the end to ensure that 555 // the ContactProvider already knows about newly-created contacts. 556 if (updatedPhotos != null) { 557 for (String key : updatedPhotos.keySet()) { 558 Uri photoUri = updatedPhotos.getParcelable(key); 559 long rawContactId = Long.parseLong(key); 560 561 // If the raw-contact ID is negative, we are saving a new raw-contact; 562 // replace the bogus ID with the new one that we actually saved the contact at. 563 if (rawContactId < 0) { 564 rawContactId = insertedRawContactId; 565 } 566 567 // If the save failed, insertedRawContactId will be -1 568 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) { 569 succeeded = false; 570 } 571 } 572 } 573 574 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 575 if (callbackIntent != null) { 576 if (succeeded) { 577 // Mark the intent to indicate that the save was successful (even if the lookup URI 578 // is now null). For local contacts or the local profile, it's possible that the 579 // save triggered removal of the contact, so no lookup URI would exist.. 580 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true); 581 } 582 callbackIntent.setData(lookupUri); 583 deliverCallback(callbackIntent); 584 } 585 } 586 587 /** 588 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the 589 * subsets, adds the returned array to "results". 590 * 591 * @return the size of the array, if not null; -1 when the array is null. 592 */ 593 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset, 594 ContentProviderResult[] results, ContentResolver resolver) 595 throws RemoteException, OperationApplicationException { 596 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE); 597 final ArrayList<ContentProviderOperation> subset = new ArrayList<>(); 598 subset.addAll(diff.subList(offset, offset + subsetCount)); 599 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract 600 .AUTHORITY, subset); 601 if (subsetResult == null || (offset + subsetResult.length) > results.length) { 602 return -1; 603 } 604 for (ContentProviderResult c : subsetResult) { 605 results[offset++] = c; 606 } 607 return subsetResult.length; 608 } 609 610 /** 611 * Save updated photo for the specified raw-contact. 612 * @return true for success, false for failure 613 */ 614 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) { 615 final Uri outputUri = Uri.withAppendedPath( 616 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 617 RawContacts.DisplayPhoto.CONTENT_DIRECTORY); 618 619 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0)); 620 } 621 622 /** 623 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1. 624 */ 625 private long getRawContactId(RawContactDeltaList state, 626 final ArrayList<CPOWrapper> diffWrapper, 627 final ContentProviderResult[] results) { 628 long existingRawContactId = state.findRawContactId(); 629 if (existingRawContactId != -1) { 630 return existingRawContactId; 631 } 632 633 return getInsertedRawContactId(diffWrapper, results); 634 } 635 636 /** 637 * Find the ID of a newly-inserted raw-contact. If none exists, return -1. 638 */ 639 private long getInsertedRawContactId( 640 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) { 641 if (results == null) { 642 return -1; 643 } 644 final int diffSize = diffWrapper.size(); 645 final int numResults = results.length; 646 for (int i = 0; i < diffSize && i < numResults; i++) { 647 final CPOWrapper cpoWrapper = diffWrapper.get(i); 648 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper); 649 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains( 650 RawContacts.CONTENT_URI.getEncodedPath())) { 651 return ContentUris.parseId(results[i].uri); 652 } 653 } 654 return -1; 655 } 656 657 /** 658 * Creates an intent that can be sent to this service to create a new group as 659 * well as add new members at the same time. 660 * 661 * @param context of the application 662 * @param account in which the group should be created 663 * @param label is the name of the group (cannot be null) 664 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 665 * should be added to the group 666 * @param callbackActivity is the activity to send the callback intent to 667 * @param callbackAction is the intent action for the callback intent 668 */ 669 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account, 670 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity, 671 String callbackAction) { 672 Intent serviceIntent = new Intent(context, ContactSaveService.class); 673 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP); 674 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type); 675 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name); 676 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet); 677 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label); 678 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 679 680 // Callback intent will be invoked by the service once the new group is 681 // created. 682 Intent callbackIntent = new Intent(context, callbackActivity); 683 callbackIntent.setAction(callbackAction); 684 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 685 686 return serviceIntent; 687 } 688 689 private void createGroup(Intent intent) { 690 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); 691 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); 692 String dataSet = intent.getStringExtra(EXTRA_DATA_SET); 693 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 694 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 695 696 ContentValues values = new ContentValues(); 697 values.put(Groups.ACCOUNT_TYPE, accountType); 698 values.put(Groups.ACCOUNT_NAME, accountName); 699 values.put(Groups.DATA_SET, dataSet); 700 values.put(Groups.TITLE, label); 701 702 final ContentResolver resolver = getContentResolver(); 703 704 // Create the new group 705 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values); 706 707 // If there's no URI, then the insertion failed. Abort early because group members can't be 708 // added if the group doesn't exist 709 if (groupUri == null) { 710 Log.e(TAG, "Couldn't create group with label " + label); 711 return; 712 } 713 714 // Add new group members 715 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri)); 716 717 // TODO: Move this into the contact editor where it belongs. This needs to be integrated 718 // with the way other intent extras that are passed to the {@link ContactEditorActivity}. 719 values.clear(); 720 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 721 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri)); 722 723 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 724 callbackIntent.setData(groupUri); 725 // TODO: This can be taken out when the above TODO is addressed 726 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values)); 727 deliverCallback(callbackIntent); 728 } 729 730 /** 731 * Creates an intent that can be sent to this service to rename a group. 732 */ 733 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel, 734 Class<? extends Activity> callbackActivity, String callbackAction) { 735 Intent serviceIntent = new Intent(context, ContactSaveService.class); 736 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP); 737 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 738 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 739 740 // Callback intent will be invoked by the service once the group is renamed. 741 Intent callbackIntent = new Intent(context, callbackActivity); 742 callbackIntent.setAction(callbackAction); 743 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 744 745 return serviceIntent; 746 } 747 748 private void renameGroup(Intent intent) { 749 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 750 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 751 752 if (groupId == -1) { 753 Log.e(TAG, "Invalid arguments for renameGroup request"); 754 return; 755 } 756 757 ContentValues values = new ContentValues(); 758 values.put(Groups.TITLE, label); 759 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 760 getContentResolver().update(groupUri, values, null, null); 761 762 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 763 callbackIntent.setData(groupUri); 764 deliverCallback(callbackIntent); 765 } 766 767 /** 768 * Creates an intent that can be sent to this service to delete a group. 769 */ 770 public static Intent createGroupDeletionIntent(Context context, long groupId) { 771 Intent serviceIntent = new Intent(context, ContactSaveService.class); 772 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP); 773 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 774 return serviceIntent; 775 } 776 777 private void deleteGroup(Intent intent) { 778 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 779 if (groupId == -1) { 780 Log.e(TAG, "Invalid arguments for deleteGroup request"); 781 return; 782 } 783 784 getContentResolver().delete( 785 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null); 786 } 787 788 /** 789 * Creates an intent that can be sent to this service to rename a group as 790 * well as add and remove members from the group. 791 * 792 * @param context of the application 793 * @param groupId of the group that should be modified 794 * @param newLabel is the updated name of the group (can be null if the name 795 * should not be updated) 796 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 797 * should be added to the group 798 * @param rawContactsToRemove is an array of raw contact IDs for contacts 799 * that should be removed from the group 800 * @param callbackActivity is the activity to send the callback intent to 801 * @param callbackAction is the intent action for the callback intent 802 */ 803 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel, 804 long[] rawContactsToAdd, long[] rawContactsToRemove, 805 Class<? extends Activity> callbackActivity, String callbackAction) { 806 Intent serviceIntent = new Intent(context, ContactSaveService.class); 807 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP); 808 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 809 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 810 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 811 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE, 812 rawContactsToRemove); 813 814 // Callback intent will be invoked by the service once the group is updated 815 Intent callbackIntent = new Intent(context, callbackActivity); 816 callbackIntent.setAction(callbackAction); 817 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 818 819 return serviceIntent; 820 } 821 822 private void updateGroup(Intent intent) { 823 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 824 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 825 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 826 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE); 827 828 if (groupId == -1) { 829 Log.e(TAG, "Invalid arguments for updateGroup request"); 830 return; 831 } 832 833 final ContentResolver resolver = getContentResolver(); 834 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 835 836 // Update group name if necessary 837 if (label != null) { 838 ContentValues values = new ContentValues(); 839 values.put(Groups.TITLE, label); 840 resolver.update(groupUri, values, null, null); 841 } 842 843 // Add and remove members if necessary 844 addMembersToGroup(resolver, rawContactsToAdd, groupId); 845 removeMembersFromGroup(resolver, rawContactsToRemove, groupId); 846 847 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 848 callbackIntent.setData(groupUri); 849 deliverCallback(callbackIntent); 850 } 851 852 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd, 853 long groupId) { 854 if (rawContactsToAdd == null) { 855 return; 856 } 857 for (long rawContactId : rawContactsToAdd) { 858 try { 859 final ArrayList<ContentProviderOperation> rawContactOperations = 860 new ArrayList<ContentProviderOperation>(); 861 862 // Build an assert operation to ensure the contact is not already in the group 863 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation 864 .newAssertQuery(Data.CONTENT_URI); 865 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " + 866 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 867 new String[] { String.valueOf(rawContactId), 868 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 869 assertBuilder.withExpectedCount(0); 870 rawContactOperations.add(assertBuilder.build()); 871 872 // Build an insert operation to add the contact to the group 873 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation 874 .newInsert(Data.CONTENT_URI); 875 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId); 876 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 877 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId); 878 rawContactOperations.add(insertBuilder.build()); 879 880 if (DEBUG) { 881 for (ContentProviderOperation operation : rawContactOperations) { 882 Log.v(TAG, operation.toString()); 883 } 884 } 885 886 // Apply batch 887 if (!rawContactOperations.isEmpty()) { 888 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations); 889 } 890 } catch (RemoteException e) { 891 // Something went wrong, bail without success 892 Log.e(TAG, "Problem persisting user edits for raw contact ID " + 893 String.valueOf(rawContactId), e); 894 } catch (OperationApplicationException e) { 895 // The assert could have failed because the contact is already in the group, 896 // just continue to the next contact 897 Log.w(TAG, "Assert failed in adding raw contact ID " + 898 String.valueOf(rawContactId) + ". Already exists in group " + 899 String.valueOf(groupId), e); 900 } 901 } 902 } 903 904 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove, 905 long groupId) { 906 if (rawContactsToRemove == null) { 907 return; 908 } 909 for (long rawContactId : rawContactsToRemove) { 910 // Apply the delete operation on the data row for the given raw contact's 911 // membership in the given group. If no contact matches the provided selection, then 912 // nothing will be done. Just continue to the next contact. 913 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " + 914 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 915 new String[] { String.valueOf(rawContactId), 916 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 917 } 918 } 919 920 /** 921 * Creates an intent that can be sent to this service to star or un-star a contact. 922 */ 923 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) { 924 Intent serviceIntent = new Intent(context, ContactSaveService.class); 925 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED); 926 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 927 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value); 928 929 return serviceIntent; 930 } 931 932 private void setStarred(Intent intent) { 933 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 934 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false); 935 if (contactUri == null) { 936 Log.e(TAG, "Invalid arguments for setStarred request"); 937 return; 938 } 939 940 final ContentValues values = new ContentValues(1); 941 values.put(Contacts.STARRED, value); 942 getContentResolver().update(contactUri, values, null, null); 943 944 // Undemote the contact if necessary 945 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID}, 946 null, null, null); 947 if (c == null) { 948 return; 949 } 950 try { 951 if (c.moveToFirst()) { 952 final long id = c.getLong(0); 953 954 // Don't bother undemoting if this contact is the user's profile. 955 if (id < Profile.MIN_ID) { 956 PinnedPositionsCompat.undemote(getContentResolver(), id); 957 } 958 } 959 } finally { 960 c.close(); 961 } 962 } 963 964 /** 965 * Creates an intent that can be sent to this service to set the redirect to voicemail. 966 */ 967 public static Intent createSetSendToVoicemail(Context context, Uri contactUri, 968 boolean value) { 969 Intent serviceIntent = new Intent(context, ContactSaveService.class); 970 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL); 971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 972 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value); 973 974 return serviceIntent; 975 } 976 977 private void setSendToVoicemail(Intent intent) { 978 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 979 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false); 980 if (contactUri == null) { 981 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail"); 982 return; 983 } 984 985 final ContentValues values = new ContentValues(1); 986 values.put(Contacts.SEND_TO_VOICEMAIL, value); 987 getContentResolver().update(contactUri, values, null, null); 988 } 989 990 /** 991 * Creates an intent that can be sent to this service to save the contact's ringtone. 992 */ 993 public static Intent createSetRingtone(Context context, Uri contactUri, 994 String value) { 995 Intent serviceIntent = new Intent(context, ContactSaveService.class); 996 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE); 997 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 998 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value); 999 1000 return serviceIntent; 1001 } 1002 1003 private void setRingtone(Intent intent) { 1004 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1005 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE); 1006 if (contactUri == null) { 1007 Log.e(TAG, "Invalid arguments for setRingtone"); 1008 return; 1009 } 1010 ContentValues values = new ContentValues(1); 1011 values.put(Contacts.CUSTOM_RINGTONE, value); 1012 getContentResolver().update(contactUri, values, null, null); 1013 } 1014 1015 /** 1016 * Creates an intent that sets the selected data item as super primary (default) 1017 */ 1018 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) { 1019 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1020 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY); 1021 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 1022 return serviceIntent; 1023 } 1024 1025 private void setSuperPrimary(Intent intent) { 1026 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 1027 if (dataId == -1) { 1028 Log.e(TAG, "Invalid arguments for setSuperPrimary request"); 1029 return; 1030 } 1031 1032 ContactUpdateUtils.setSuperPrimary(this, dataId); 1033 } 1034 1035 /** 1036 * Creates an intent that clears the primary flag of all data items that belong to the same 1037 * raw_contact as the given data item. Will only clear, if the data item was primary before 1038 * this call 1039 */ 1040 public static Intent createClearPrimaryIntent(Context context, long dataId) { 1041 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1042 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY); 1043 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 1044 return serviceIntent; 1045 } 1046 1047 private void clearPrimary(Intent intent) { 1048 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 1049 if (dataId == -1) { 1050 Log.e(TAG, "Invalid arguments for clearPrimary request"); 1051 return; 1052 } 1053 1054 // Update the primary values in the data record. 1055 ContentValues values = new ContentValues(1); 1056 values.put(Data.IS_SUPER_PRIMARY, 0); 1057 values.put(Data.IS_PRIMARY, 0); 1058 1059 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), 1060 values, null, null); 1061 } 1062 1063 /** 1064 * Creates an intent that can be sent to this service to delete a contact. 1065 */ 1066 public static Intent createDeleteContactIntent(Context context, Uri contactUri) { 1067 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1068 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT); 1069 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 1070 return serviceIntent; 1071 } 1072 1073 /** 1074 * Creates an intent that can be sent to this service to delete multiple contacts. 1075 */ 1076 public static Intent createDeleteMultipleContactsIntent(Context context, 1077 long[] contactIds) { 1078 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1079 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS); 1080 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds); 1081 return serviceIntent; 1082 } 1083 1084 private void deleteContact(Intent intent) { 1085 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1086 if (contactUri == null) { 1087 Log.e(TAG, "Invalid arguments for deleteContact request"); 1088 return; 1089 } 1090 1091 getContentResolver().delete(contactUri, null, null); 1092 } 1093 1094 private void deleteMultipleContacts(Intent intent) { 1095 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS); 1096 if (contactIds == null) { 1097 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request"); 1098 return; 1099 } 1100 for (long contactId : contactIds) { 1101 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 1102 getContentResolver().delete(contactUri, null, null); 1103 } 1104 final String deleteToastMessage = getResources().getQuantityString(R.plurals 1105 .contacts_deleted_toast, contactIds.length); 1106 mMainHandler.post(new Runnable() { 1107 @Override 1108 public void run() { 1109 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG) 1110 .show(); 1111 } 1112 }); 1113 } 1114 1115 /** 1116 * Creates an intent that can be sent to this service to join two contacts. 1117 * The resulting contact uses the name from {@param contactId1} if possible. 1118 */ 1119 public static Intent createJoinContactsIntent(Context context, long contactId1, 1120 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) { 1121 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1122 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS); 1123 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1); 1124 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2); 1125 1126 // Callback intent will be invoked by the service once the contacts are joined. 1127 Intent callbackIntent = new Intent(context, callbackActivity); 1128 callbackIntent.setAction(callbackAction); 1129 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 1130 1131 return serviceIntent; 1132 } 1133 1134 /** 1135 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts. 1136 * No special attention is paid to where the resulting contact's name is taken from. 1137 */ 1138 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) { 1139 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1140 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS); 1141 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds); 1142 return serviceIntent; 1143 } 1144 1145 1146 private interface JoinContactQuery { 1147 String[] PROJECTION = { 1148 RawContacts._ID, 1149 RawContacts.CONTACT_ID, 1150 RawContacts.DISPLAY_NAME_SOURCE, 1151 }; 1152 1153 int _ID = 0; 1154 int CONTACT_ID = 1; 1155 int DISPLAY_NAME_SOURCE = 2; 1156 } 1157 1158 private interface ContactEntityQuery { 1159 String[] PROJECTION = { 1160 Contacts.Entity.DATA_ID, 1161 Contacts.Entity.CONTACT_ID, 1162 Contacts.Entity.IS_SUPER_PRIMARY, 1163 }; 1164 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" + 1165 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME + 1166 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " + 1167 " AND " + StructuredName.DISPLAY_NAME + " != '' "; 1168 1169 int DATA_ID = 0; 1170 int CONTACT_ID = 1; 1171 int IS_SUPER_PRIMARY = 2; 1172 } 1173 1174 private void joinSeveralContacts(Intent intent) { 1175 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS); 1176 1177 // Load raw contact IDs for all contacts involved. 1178 long rawContactIds[] = getRawContactIdsForAggregation(contactIds); 1179 if (rawContactIds == null) { 1180 Log.e(TAG, "Invalid arguments for joinSeveralContacts request"); 1181 return; 1182 } 1183 1184 // For each pair of raw contacts, insert an aggregation exception 1185 final ContentResolver resolver = getContentResolver(); 1186 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225 1187 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE; 1188 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize); 1189 for (int i = 0; i < rawContactIds.length; i++) { 1190 for (int j = 0; j < rawContactIds.length; j++) { 1191 if (i != j) { 1192 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 1193 } 1194 // Before we get to 500 we need to flush the operations list 1195 if (operations.size() > 0 && operations.size() % batchSize == 0) { 1196 if (!applyJoinOperations(resolver, operations)) { 1197 return; 1198 } 1199 operations.clear(); 1200 } 1201 } 1202 } 1203 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) { 1204 return; 1205 } 1206 showToast(R.string.contactsJoinedMessage); 1207 } 1208 1209 /** Returns true if the batch was successfully applied and false otherwise. */ 1210 private boolean applyJoinOperations(ContentResolver resolver, 1211 ArrayList<ContentProviderOperation> operations) { 1212 try { 1213 resolver.applyBatch(ContactsContract.AUTHORITY, operations); 1214 return true; 1215 } catch (RemoteException | OperationApplicationException e) { 1216 Log.e(TAG, "Failed to apply aggregation exception batch", e); 1217 showToast(R.string.contactSavedErrorToast); 1218 return false; 1219 } 1220 } 1221 1222 1223 private void joinContacts(Intent intent) { 1224 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1); 1225 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1); 1226 1227 // Load raw contact IDs for all raw contacts involved - currently edited and selected 1228 // in the join UIs. 1229 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2); 1230 if (rawContactIds == null) { 1231 Log.e(TAG, "Invalid arguments for joinContacts request"); 1232 return; 1233 } 1234 1235 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 1236 1237 // For each pair of raw contacts, insert an aggregation exception 1238 for (int i = 0; i < rawContactIds.length; i++) { 1239 for (int j = 0; j < rawContactIds.length; j++) { 1240 if (i != j) { 1241 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 1242 } 1243 } 1244 } 1245 1246 final ContentResolver resolver = getContentResolver(); 1247 1248 // Use the name for contactId1 as the name for the newly aggregated contact. 1249 final Uri contactId1Uri = ContentUris.withAppendedId( 1250 Contacts.CONTENT_URI, contactId1); 1251 final Uri entityUri = Uri.withAppendedPath( 1252 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY); 1253 Cursor c = resolver.query(entityUri, 1254 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null); 1255 if (c == null) { 1256 Log.e(TAG, "Unable to open Contacts DB cursor"); 1257 showToast(R.string.contactSavedErrorToast); 1258 return; 1259 } 1260 long dataIdToAddSuperPrimary = -1; 1261 try { 1262 if (c.moveToFirst()) { 1263 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID); 1264 } 1265 } finally { 1266 c.close(); 1267 } 1268 1269 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact 1270 // display name does not change as a result of the join. 1271 if (dataIdToAddSuperPrimary != -1) { 1272 Builder builder = ContentProviderOperation.newUpdate( 1273 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary)); 1274 builder.withValue(Data.IS_SUPER_PRIMARY, 1); 1275 builder.withValue(Data.IS_PRIMARY, 1); 1276 operations.add(builder.build()); 1277 } 1278 1279 boolean success = false; 1280 // Apply all aggregation exceptions as one batch 1281 try { 1282 resolver.applyBatch(ContactsContract.AUTHORITY, operations); 1283 showToast(R.string.contactsJoinedMessage); 1284 success = true; 1285 } catch (RemoteException | OperationApplicationException e) { 1286 Log.e(TAG, "Failed to apply aggregation exception batch", e); 1287 showToast(R.string.contactSavedErrorToast); 1288 } 1289 1290 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 1291 if (success) { 1292 Uri uri = RawContacts.getContactLookupUri(resolver, 1293 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0])); 1294 callbackIntent.setData(uri); 1295 } 1296 deliverCallback(callbackIntent); 1297 } 1298 1299 private long[] getRawContactIdsForAggregation(long[] contactIds) { 1300 if (contactIds == null) { 1301 return null; 1302 } 1303 1304 final ContentResolver resolver = getContentResolver(); 1305 long rawContactIds[]; 1306 1307 final StringBuilder queryBuilder = new StringBuilder(); 1308 final String stringContactIds[] = new String[contactIds.length]; 1309 for (int i = 0; i < contactIds.length; i++) { 1310 queryBuilder.append(RawContacts.CONTACT_ID + "=?"); 1311 stringContactIds[i] = String.valueOf(contactIds[i]); 1312 if (contactIds[i] == -1) { 1313 return null; 1314 } 1315 if (i == contactIds.length -1) { 1316 break; 1317 } 1318 queryBuilder.append(" OR "); 1319 } 1320 1321 final Cursor c = resolver.query(RawContacts.CONTENT_URI, 1322 JoinContactQuery.PROJECTION, 1323 queryBuilder.toString(), 1324 stringContactIds, null); 1325 if (c == null) { 1326 Log.e(TAG, "Unable to open Contacts DB cursor"); 1327 showToast(R.string.contactSavedErrorToast); 1328 return null; 1329 } 1330 try { 1331 if (c.getCount() < 2) { 1332 Log.e(TAG, "Not enough raw contacts to aggregate together."); 1333 return null; 1334 } 1335 rawContactIds = new long[c.getCount()]; 1336 for (int i = 0; i < rawContactIds.length; i++) { 1337 c.moveToPosition(i); 1338 long rawContactId = c.getLong(JoinContactQuery._ID); 1339 rawContactIds[i] = rawContactId; 1340 } 1341 } finally { 1342 c.close(); 1343 } 1344 return rawContactIds; 1345 } 1346 1347 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) { 1348 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2}); 1349 } 1350 1351 /** 1352 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation. 1353 */ 1354 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, 1355 long rawContactId1, long rawContactId2) { 1356 Builder builder = 1357 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 1358 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); 1359 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 1360 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 1361 operations.add(builder.build()); 1362 } 1363 1364 /** 1365 * Shows a toast on the UI thread. 1366 */ 1367 private void showToast(final int message) { 1368 mMainHandler.post(new Runnable() { 1369 1370 @Override 1371 public void run() { 1372 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show(); 1373 } 1374 }); 1375 } 1376 1377 private void deliverCallback(final Intent callbackIntent) { 1378 mMainHandler.post(new Runnable() { 1379 1380 @Override 1381 public void run() { 1382 deliverCallbackOnUiThread(callbackIntent); 1383 } 1384 }); 1385 } 1386 1387 void deliverCallbackOnUiThread(final Intent callbackIntent) { 1388 // TODO: this assumes that if there are multiple instances of the same 1389 // activity registered, the last one registered is the one waiting for 1390 // the callback. Validity of this assumption needs to be verified. 1391 for (Listener listener : sListeners) { 1392 if (callbackIntent.getComponent().equals( 1393 ((Activity) listener).getIntent().getComponent())) { 1394 listener.onServiceCompleted(callbackIntent); 1395 return; 1396 } 1397 } 1398 } 1399 } 1400