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