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