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