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