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