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.model.RawContactDelta; 53 import com.android.contacts.model.RawContactDeltaList; 54 import com.android.contacts.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 (OperationApplicationException e) { 414 // Version consistency failed, re-parent change and try again 415 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString()); 416 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN("); 417 boolean first = true; 418 final int count = state.size(); 419 for (int i = 0; i < count; i++) { 420 Long rawContactId = state.getRawContactId(i); 421 if (rawContactId != null && rawContactId != -1) { 422 if (!first) { 423 sb.append(','); 424 } 425 sb.append(rawContactId); 426 first = false; 427 } 428 } 429 sb.append(")"); 430 431 if (first) { 432 throw new IllegalStateException("Version consistency failed for a new contact"); 433 } 434 435 final RawContactDeltaList newState = RawContactDeltaList.fromQuery( 436 isProfile 437 ? RawContactsEntity.PROFILE_CONTENT_URI 438 : RawContactsEntity.CONTENT_URI, 439 resolver, sb.toString(), null, null); 440 state = RawContactDeltaList.mergeAfter(newState, state); 441 442 // Update the new state to use profile URIs if appropriate. 443 if (isProfile) { 444 for (RawContactDelta delta : state) { 445 delta.setProfileQueryUri(); 446 } 447 } 448 } 449 } 450 451 // Now save any updated photos. We do this at the end to ensure that 452 // the ContactProvider already knows about newly-created contacts. 453 if (updatedPhotos != null) { 454 for (String key : updatedPhotos.keySet()) { 455 Uri photoUri = updatedPhotos.getParcelable(key); 456 long rawContactId = Long.parseLong(key); 457 458 // If the raw-contact ID is negative, we are saving a new raw-contact; 459 // replace the bogus ID with the new one that we actually saved the contact at. 460 if (rawContactId < 0) { 461 rawContactId = insertedRawContactId; 462 if (rawContactId == -1) { 463 throw new IllegalStateException( 464 "Could not determine RawContact ID for image insertion"); 465 } 466 } 467 468 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false; 469 } 470 } 471 472 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 473 if (callbackIntent != null) { 474 if (succeeded) { 475 // Mark the intent to indicate that the save was successful (even if the lookup URI 476 // is now null). For local contacts or the local profile, it's possible that the 477 // save triggered removal of the contact, so no lookup URI would exist.. 478 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true); 479 } 480 callbackIntent.setData(lookupUri); 481 deliverCallback(callbackIntent); 482 } 483 } 484 485 /** 486 * Save updated photo for the specified raw-contact. 487 * @return true for success, false for failure 488 */ 489 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) { 490 final Uri outputUri = Uri.withAppendedPath( 491 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 492 RawContacts.DisplayPhoto.CONTENT_DIRECTORY); 493 494 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true); 495 } 496 497 /** 498 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1. 499 */ 500 private long getRawContactId(RawContactDeltaList state, 501 final ArrayList<ContentProviderOperation> diff, 502 final ContentProviderResult[] results) { 503 long existingRawContactId = state.findRawContactId(); 504 if (existingRawContactId != -1) { 505 return existingRawContactId; 506 } 507 508 return getInsertedRawContactId(diff, results); 509 } 510 511 /** 512 * Find the ID of a newly-inserted raw-contact. If none exists, return -1. 513 */ 514 private long getInsertedRawContactId( 515 final ArrayList<ContentProviderOperation> diff, 516 final ContentProviderResult[] results) { 517 final int diffSize = diff.size(); 518 for (int i = 0; i < diffSize; i++) { 519 ContentProviderOperation operation = diff.get(i); 520 if (operation.getType() == ContentProviderOperation.TYPE_INSERT 521 && operation.getUri().getEncodedPath().contains( 522 RawContacts.CONTENT_URI.getEncodedPath())) { 523 return ContentUris.parseId(results[i].uri); 524 } 525 } 526 return -1; 527 } 528 529 /** 530 * Creates an intent that can be sent to this service to create a new group as 531 * well as add new members at the same time. 532 * 533 * @param context of the application 534 * @param account in which the group should be created 535 * @param label is the name of the group (cannot be null) 536 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 537 * should be added to the group 538 * @param callbackActivity is the activity to send the callback intent to 539 * @param callbackAction is the intent action for the callback intent 540 */ 541 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account, 542 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity, 543 String callbackAction) { 544 Intent serviceIntent = new Intent(context, ContactSaveService.class); 545 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP); 546 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type); 547 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name); 548 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet); 549 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label); 550 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 551 552 // Callback intent will be invoked by the service once the new group is 553 // created. 554 Intent callbackIntent = new Intent(context, callbackActivity); 555 callbackIntent.setAction(callbackAction); 556 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 557 558 return serviceIntent; 559 } 560 561 private void createGroup(Intent intent) { 562 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); 563 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); 564 String dataSet = intent.getStringExtra(EXTRA_DATA_SET); 565 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 566 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 567 568 ContentValues values = new ContentValues(); 569 values.put(Groups.ACCOUNT_TYPE, accountType); 570 values.put(Groups.ACCOUNT_NAME, accountName); 571 values.put(Groups.DATA_SET, dataSet); 572 values.put(Groups.TITLE, label); 573 574 final ContentResolver resolver = getContentResolver(); 575 576 // Create the new group 577 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values); 578 579 // If there's no URI, then the insertion failed. Abort early because group members can't be 580 // added if the group doesn't exist 581 if (groupUri == null) { 582 Log.e(TAG, "Couldn't create group with label " + label); 583 return; 584 } 585 586 // Add new group members 587 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri)); 588 589 // TODO: Move this into the contact editor where it belongs. This needs to be integrated 590 // with the way other intent extras that are passed to the {@link ContactEditorActivity}. 591 values.clear(); 592 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 593 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri)); 594 595 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 596 callbackIntent.setData(groupUri); 597 // TODO: This can be taken out when the above TODO is addressed 598 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values)); 599 deliverCallback(callbackIntent); 600 } 601 602 /** 603 * Creates an intent that can be sent to this service to rename a group. 604 */ 605 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel, 606 Class<? extends Activity> callbackActivity, String callbackAction) { 607 Intent serviceIntent = new Intent(context, ContactSaveService.class); 608 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP); 609 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 610 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 611 612 // Callback intent will be invoked by the service once the group is renamed. 613 Intent callbackIntent = new Intent(context, callbackActivity); 614 callbackIntent.setAction(callbackAction); 615 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 616 617 return serviceIntent; 618 } 619 620 private void renameGroup(Intent intent) { 621 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 622 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 623 624 if (groupId == -1) { 625 Log.e(TAG, "Invalid arguments for renameGroup request"); 626 return; 627 } 628 629 ContentValues values = new ContentValues(); 630 values.put(Groups.TITLE, label); 631 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 632 getContentResolver().update(groupUri, values, null, null); 633 634 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 635 callbackIntent.setData(groupUri); 636 deliverCallback(callbackIntent); 637 } 638 639 /** 640 * Creates an intent that can be sent to this service to delete a group. 641 */ 642 public static Intent createGroupDeletionIntent(Context context, long groupId) { 643 Intent serviceIntent = new Intent(context, ContactSaveService.class); 644 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP); 645 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 646 return serviceIntent; 647 } 648 649 private void deleteGroup(Intent intent) { 650 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 651 if (groupId == -1) { 652 Log.e(TAG, "Invalid arguments for deleteGroup request"); 653 return; 654 } 655 656 getContentResolver().delete( 657 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null); 658 } 659 660 /** 661 * Creates an intent that can be sent to this service to rename a group as 662 * well as add and remove members from the group. 663 * 664 * @param context of the application 665 * @param groupId of the group that should be modified 666 * @param newLabel is the updated name of the group (can be null if the name 667 * should not be updated) 668 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 669 * should be added to the group 670 * @param rawContactsToRemove is an array of raw contact IDs for contacts 671 * that should be removed from the group 672 * @param callbackActivity is the activity to send the callback intent to 673 * @param callbackAction is the intent action for the callback intent 674 */ 675 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel, 676 long[] rawContactsToAdd, long[] rawContactsToRemove, 677 Class<? extends Activity> callbackActivity, String callbackAction) { 678 Intent serviceIntent = new Intent(context, ContactSaveService.class); 679 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP); 680 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 681 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 682 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 683 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE, 684 rawContactsToRemove); 685 686 // Callback intent will be invoked by the service once the group is updated 687 Intent callbackIntent = new Intent(context, callbackActivity); 688 callbackIntent.setAction(callbackAction); 689 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 690 691 return serviceIntent; 692 } 693 694 private void updateGroup(Intent intent) { 695 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 696 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 697 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 698 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE); 699 700 if (groupId == -1) { 701 Log.e(TAG, "Invalid arguments for updateGroup request"); 702 return; 703 } 704 705 final ContentResolver resolver = getContentResolver(); 706 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 707 708 // Update group name if necessary 709 if (label != null) { 710 ContentValues values = new ContentValues(); 711 values.put(Groups.TITLE, label); 712 resolver.update(groupUri, values, null, null); 713 } 714 715 // Add and remove members if necessary 716 addMembersToGroup(resolver, rawContactsToAdd, groupId); 717 removeMembersFromGroup(resolver, rawContactsToRemove, groupId); 718 719 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 720 callbackIntent.setData(groupUri); 721 deliverCallback(callbackIntent); 722 } 723 724 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd, 725 long groupId) { 726 if (rawContactsToAdd == null) { 727 return; 728 } 729 for (long rawContactId : rawContactsToAdd) { 730 try { 731 final ArrayList<ContentProviderOperation> rawContactOperations = 732 new ArrayList<ContentProviderOperation>(); 733 734 // Build an assert operation to ensure the contact is not already in the group 735 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation 736 .newAssertQuery(Data.CONTENT_URI); 737 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " + 738 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 739 new String[] { String.valueOf(rawContactId), 740 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 741 assertBuilder.withExpectedCount(0); 742 rawContactOperations.add(assertBuilder.build()); 743 744 // Build an insert operation to add the contact to the group 745 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation 746 .newInsert(Data.CONTENT_URI); 747 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId); 748 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 749 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId); 750 rawContactOperations.add(insertBuilder.build()); 751 752 if (DEBUG) { 753 for (ContentProviderOperation operation : rawContactOperations) { 754 Log.v(TAG, operation.toString()); 755 } 756 } 757 758 // Apply batch 759 if (!rawContactOperations.isEmpty()) { 760 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations); 761 } 762 } catch (RemoteException e) { 763 // Something went wrong, bail without success 764 Log.e(TAG, "Problem persisting user edits for raw contact ID " + 765 String.valueOf(rawContactId), e); 766 } catch (OperationApplicationException e) { 767 // The assert could have failed because the contact is already in the group, 768 // just continue to the next contact 769 Log.w(TAG, "Assert failed in adding raw contact ID " + 770 String.valueOf(rawContactId) + ". Already exists in group " + 771 String.valueOf(groupId), e); 772 } 773 } 774 } 775 776 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove, 777 long groupId) { 778 if (rawContactsToRemove == null) { 779 return; 780 } 781 for (long rawContactId : rawContactsToRemove) { 782 // Apply the delete operation on the data row for the given raw contact's 783 // membership in the given group. If no contact matches the provided selection, then 784 // nothing will be done. Just continue to the next contact. 785 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " + 786 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 787 new String[] { String.valueOf(rawContactId), 788 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 789 } 790 } 791 792 /** 793 * Creates an intent that can be sent to this service to star or un-star a contact. 794 */ 795 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) { 796 Intent serviceIntent = new Intent(context, ContactSaveService.class); 797 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED); 798 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 799 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value); 800 801 return serviceIntent; 802 } 803 804 private void setStarred(Intent intent) { 805 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 806 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false); 807 if (contactUri == null) { 808 Log.e(TAG, "Invalid arguments for setStarred request"); 809 return; 810 } 811 812 final ContentValues values = new ContentValues(1); 813 values.put(Contacts.STARRED, value); 814 getContentResolver().update(contactUri, values, null, null); 815 816 // Undemote the contact if necessary 817 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID}, 818 null, null, null); 819 try { 820 if (c.moveToFirst()) { 821 final long id = c.getLong(0); 822 823 // Don't bother undemoting if this contact is the user's profile. 824 if (id < Profile.MIN_ID) { 825 values.clear(); 826 values.put(String.valueOf(id), PinnedPositions.UNDEMOTE); 827 getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null); 828 } 829 } 830 } finally { 831 c.close(); 832 } 833 } 834 835 /** 836 * Creates an intent that can be sent to this service to set the redirect to voicemail. 837 */ 838 public static Intent createSetSendToVoicemail(Context context, Uri contactUri, 839 boolean value) { 840 Intent serviceIntent = new Intent(context, ContactSaveService.class); 841 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL); 842 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 843 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value); 844 845 return serviceIntent; 846 } 847 848 private void setSendToVoicemail(Intent intent) { 849 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 850 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false); 851 if (contactUri == null) { 852 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail"); 853 return; 854 } 855 856 final ContentValues values = new ContentValues(1); 857 values.put(Contacts.SEND_TO_VOICEMAIL, value); 858 getContentResolver().update(contactUri, values, null, null); 859 } 860 861 /** 862 * Creates an intent that can be sent to this service to save the contact's ringtone. 863 */ 864 public static Intent createSetRingtone(Context context, Uri contactUri, 865 String value) { 866 Intent serviceIntent = new Intent(context, ContactSaveService.class); 867 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE); 868 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 869 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value); 870 871 return serviceIntent; 872 } 873 874 private void setRingtone(Intent intent) { 875 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 876 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE); 877 if (contactUri == null) { 878 Log.e(TAG, "Invalid arguments for setRingtone"); 879 return; 880 } 881 ContentValues values = new ContentValues(1); 882 values.put(Contacts.CUSTOM_RINGTONE, value); 883 getContentResolver().update(contactUri, values, null, null); 884 } 885 886 /** 887 * Creates an intent that sets the selected data item as super primary (default) 888 */ 889 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) { 890 Intent serviceIntent = new Intent(context, ContactSaveService.class); 891 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY); 892 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 893 return serviceIntent; 894 } 895 896 private void setSuperPrimary(Intent intent) { 897 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 898 if (dataId == -1) { 899 Log.e(TAG, "Invalid arguments for setSuperPrimary request"); 900 return; 901 } 902 903 ContactUpdateUtils.setSuperPrimary(this, dataId); 904 } 905 906 /** 907 * Creates an intent that clears the primary flag of all data items that belong to the same 908 * raw_contact as the given data item. Will only clear, if the data item was primary before 909 * this call 910 */ 911 public static Intent createClearPrimaryIntent(Context context, long dataId) { 912 Intent serviceIntent = new Intent(context, ContactSaveService.class); 913 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY); 914 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 915 return serviceIntent; 916 } 917 918 private void clearPrimary(Intent intent) { 919 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 920 if (dataId == -1) { 921 Log.e(TAG, "Invalid arguments for clearPrimary request"); 922 return; 923 } 924 925 // Update the primary values in the data record. 926 ContentValues values = new ContentValues(1); 927 values.put(Data.IS_SUPER_PRIMARY, 0); 928 values.put(Data.IS_PRIMARY, 0); 929 930 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), 931 values, null, null); 932 } 933 934 /** 935 * Creates an intent that can be sent to this service to delete a contact. 936 */ 937 public static Intent createDeleteContactIntent(Context context, Uri contactUri) { 938 Intent serviceIntent = new Intent(context, ContactSaveService.class); 939 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT); 940 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 941 return serviceIntent; 942 } 943 944 private void deleteContact(Intent intent) { 945 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 946 if (contactUri == null) { 947 Log.e(TAG, "Invalid arguments for deleteContact request"); 948 return; 949 } 950 951 getContentResolver().delete(contactUri, null, null); 952 } 953 954 /** 955 * Creates an intent that can be sent to this service to join two contacts. 956 */ 957 public static Intent createJoinContactsIntent(Context context, long contactId1, 958 long contactId2, boolean contactWritable, 959 Class<? extends Activity> callbackActivity, String callbackAction) { 960 Intent serviceIntent = new Intent(context, ContactSaveService.class); 961 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS); 962 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1); 963 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2); 964 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable); 965 966 // Callback intent will be invoked by the service once the contacts are joined. 967 Intent callbackIntent = new Intent(context, callbackActivity); 968 callbackIntent.setAction(callbackAction); 969 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 970 971 return serviceIntent; 972 } 973 974 975 private interface JoinContactQuery { 976 String[] PROJECTION = { 977 RawContacts._ID, 978 RawContacts.CONTACT_ID, 979 RawContacts.NAME_VERIFIED, 980 RawContacts.DISPLAY_NAME_SOURCE, 981 }; 982 983 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?"; 984 985 int _ID = 0; 986 int CONTACT_ID = 1; 987 int NAME_VERIFIED = 2; 988 int DISPLAY_NAME_SOURCE = 3; 989 } 990 991 private void joinContacts(Intent intent) { 992 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1); 993 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1); 994 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false); 995 if (contactId1 == -1 || contactId2 == -1) { 996 Log.e(TAG, "Invalid arguments for joinContacts request"); 997 return; 998 } 999 1000 final ContentResolver resolver = getContentResolver(); 1001 1002 // Load raw contact IDs for all raw contacts involved - currently edited and selected 1003 // in the join UIs 1004 Cursor c = resolver.query(RawContacts.CONTENT_URI, 1005 JoinContactQuery.PROJECTION, 1006 JoinContactQuery.SELECTION, 1007 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null); 1008 1009 long rawContactIds[]; 1010 long verifiedNameRawContactId = -1; 1011 try { 1012 if (c.getCount() == 0) { 1013 return; 1014 } 1015 int maxDisplayNameSource = -1; 1016 rawContactIds = new long[c.getCount()]; 1017 for (int i = 0; i < rawContactIds.length; i++) { 1018 c.moveToPosition(i); 1019 long rawContactId = c.getLong(JoinContactQuery._ID); 1020 rawContactIds[i] = rawContactId; 1021 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE); 1022 if (nameSource > maxDisplayNameSource) { 1023 maxDisplayNameSource = nameSource; 1024 } 1025 } 1026 1027 // Find an appropriate display name for the joined contact: 1028 // if should have a higher DisplayNameSource or be the name 1029 // of the original contact that we are joining with another. 1030 if (writable) { 1031 for (int i = 0; i < rawContactIds.length; i++) { 1032 c.moveToPosition(i); 1033 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) { 1034 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE); 1035 if (nameSource == maxDisplayNameSource 1036 && (verifiedNameRawContactId == -1 1037 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) { 1038 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID); 1039 } 1040 } 1041 } 1042 } 1043 } finally { 1044 c.close(); 1045 } 1046 1047 // For each pair of raw contacts, insert an aggregation exception 1048 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 1049 for (int i = 0; i < rawContactIds.length; i++) { 1050 for (int j = 0; j < rawContactIds.length; j++) { 1051 if (i != j) { 1052 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 1053 } 1054 } 1055 } 1056 1057 // Mark the original contact as "name verified" to make sure that the contact 1058 // display name does not change as a result of the join 1059 if (verifiedNameRawContactId != -1) { 1060 Builder builder = ContentProviderOperation.newUpdate( 1061 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId)); 1062 builder.withValue(RawContacts.NAME_VERIFIED, 1); 1063 operations.add(builder.build()); 1064 } 1065 1066 boolean success = false; 1067 // Apply all aggregation exceptions as one batch 1068 try { 1069 resolver.applyBatch(ContactsContract.AUTHORITY, operations); 1070 showToast(R.string.contactsJoinedMessage); 1071 success = true; 1072 } catch (RemoteException e) { 1073 Log.e(TAG, "Failed to apply aggregation exception batch", e); 1074 showToast(R.string.contactSavedErrorToast); 1075 } catch (OperationApplicationException e) { 1076 Log.e(TAG, "Failed to apply aggregation exception batch", e); 1077 showToast(R.string.contactSavedErrorToast); 1078 } 1079 1080 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 1081 if (success) { 1082 Uri uri = RawContacts.getContactLookupUri(resolver, 1083 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0])); 1084 callbackIntent.setData(uri); 1085 } 1086 deliverCallback(callbackIntent); 1087 } 1088 1089 /** 1090 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation. 1091 */ 1092 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, 1093 long rawContactId1, long rawContactId2) { 1094 Builder builder = 1095 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 1096 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); 1097 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 1098 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 1099 operations.add(builder.build()); 1100 } 1101 1102 /** 1103 * Shows a toast on the UI thread. 1104 */ 1105 private void showToast(final int message) { 1106 mMainHandler.post(new Runnable() { 1107 1108 @Override 1109 public void run() { 1110 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show(); 1111 } 1112 }); 1113 } 1114 1115 private void deliverCallback(final Intent callbackIntent) { 1116 mMainHandler.post(new Runnable() { 1117 1118 @Override 1119 public void run() { 1120 deliverCallbackOnUiThread(callbackIntent); 1121 } 1122 }); 1123 } 1124 1125 void deliverCallbackOnUiThread(final Intent callbackIntent) { 1126 // TODO: this assumes that if there are multiple instances of the same 1127 // activity registered, the last one registered is the one waiting for 1128 // the callback. Validity of this assumption needs to be verified. 1129 for (Listener listener : sListeners) { 1130 if (callbackIntent.getComponent().equals( 1131 ((Activity) listener).getIntent().getComponent())) { 1132 listener.onServiceCompleted(callbackIntent); 1133 return; 1134 } 1135 } 1136 } 1137 } 1138