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 static android.Manifest.permission.WRITE_CONTACTS; 20 21 import android.app.Activity; 22 import android.app.IntentService; 23 import android.content.ContentProviderOperation; 24 import android.content.ContentProviderOperation.Builder; 25 import android.content.ContentProviderResult; 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.OperationApplicationException; 32 import android.database.Cursor; 33 import android.database.DatabaseUtils; 34 import android.icu.text.MessageFormat; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.Parcelable; 40 import android.os.RemoteException; 41 import android.provider.ContactsContract; 42 import android.provider.ContactsContract.AggregationExceptions; 43 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 44 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 45 import android.provider.ContactsContract.Contacts; 46 import android.provider.ContactsContract.Data; 47 import android.provider.ContactsContract.Groups; 48 import android.provider.ContactsContract.Profile; 49 import android.provider.ContactsContract.RawContacts; 50 import android.provider.ContactsContract.RawContactsEntity; 51 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 52 import android.support.v4.os.ResultReceiver; 53 import android.text.TextUtils; 54 import android.util.Log; 55 import android.widget.Toast; 56 57 import com.android.contacts.activities.ContactEditorActivity; 58 import com.android.contacts.compat.CompatUtils; 59 import com.android.contacts.compat.PinnedPositionsCompat; 60 import com.android.contacts.database.ContactUpdateUtils; 61 import com.android.contacts.database.SimContactDao; 62 import com.android.contacts.model.AccountTypeManager; 63 import com.android.contacts.model.CPOWrapper; 64 import com.android.contacts.model.RawContactDelta; 65 import com.android.contacts.model.RawContactDeltaList; 66 import com.android.contacts.model.RawContactModifier; 67 import com.android.contacts.model.account.AccountWithDataSet; 68 import com.android.contacts.preference.ContactsPreferences; 69 import com.android.contacts.util.ContactDisplayUtils; 70 import com.android.contacts.util.ContactPhotoUtils; 71 import com.android.contacts.util.PermissionsUtil; 72 import com.android.contactsbind.FeedbackHelper; 73 74 import com.google.common.collect.Lists; 75 import com.google.common.collect.Sets; 76 77 import java.util.ArrayList; 78 import java.util.Collection; 79 import java.util.HashMap; 80 import java.util.HashSet; 81 import java.util.List; 82 import java.util.Locale; 83 import java.util.Map; 84 import java.util.concurrent.CopyOnWriteArrayList; 85 86 /** 87 * A service responsible for saving changes to the content provider. 88 */ 89 public class ContactSaveService extends IntentService { 90 private static final String TAG = "ContactSaveService"; 91 92 /** Set to true in order to view logs on content provider operations */ 93 private static final boolean DEBUG = false; 94 95 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact"; 96 97 public static final String EXTRA_ACCOUNT_NAME = "accountName"; 98 public static final String EXTRA_ACCOUNT_TYPE = "accountType"; 99 public static final String EXTRA_DATA_SET = "dataSet"; 100 public static final String EXTRA_ACCOUNT = "account"; 101 public static final String EXTRA_CONTENT_VALUES = "contentValues"; 102 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent"; 103 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver"; 104 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds"; 105 106 public static final String ACTION_SAVE_CONTACT = "saveContact"; 107 public static final String EXTRA_CONTACT_STATE = "state"; 108 public static final String EXTRA_SAVE_MODE = "saveMode"; 109 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile"; 110 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded"; 111 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos"; 112 113 public static final String ACTION_CREATE_GROUP = "createGroup"; 114 public static final String ACTION_RENAME_GROUP = "renameGroup"; 115 public static final String ACTION_DELETE_GROUP = "deleteGroup"; 116 public static final String ACTION_UPDATE_GROUP = "updateGroup"; 117 public static final String EXTRA_GROUP_ID = "groupId"; 118 public static final String EXTRA_GROUP_LABEL = "groupLabel"; 119 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd"; 120 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove"; 121 122 public static final String ACTION_SET_STARRED = "setStarred"; 123 public static final String ACTION_DELETE_CONTACT = "delete"; 124 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts"; 125 public static final String EXTRA_CONTACT_URI = "contactUri"; 126 public static final String EXTRA_CONTACT_IDS = "contactIds"; 127 public static final String EXTRA_STARRED_FLAG = "starred"; 128 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName"; 129 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray"; 130 131 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary"; 132 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary"; 133 public static final String EXTRA_DATA_ID = "dataId"; 134 135 public static final String ACTION_SPLIT_CONTACT = "splitContact"; 136 public static final String EXTRA_HARD_SPLIT = "extraHardSplit"; 137 138 public static final String ACTION_JOIN_CONTACTS = "joinContacts"; 139 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts"; 140 public static final String EXTRA_CONTACT_ID1 = "contactId1"; 141 public static final String EXTRA_CONTACT_ID2 = "contactId2"; 142 143 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail"; 144 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag"; 145 146 public static final String ACTION_SET_RINGTONE = "setRingtone"; 147 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone"; 148 149 public static final String ACTION_UNDO = "undo"; 150 public static final String EXTRA_UNDO_ACTION = "undoAction"; 151 public static final String EXTRA_UNDO_DATA = "undoData"; 152 153 // For debugging and testing what happens when requests are queued up. 154 public static final String ACTION_SLEEP = "sleep"; 155 public static final String EXTRA_SLEEP_DURATION = "sleepDuration"; 156 157 public static final String BROADCAST_GROUP_DELETED = "groupDeleted"; 158 public static final String BROADCAST_LINK_COMPLETE = "linkComplete"; 159 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete"; 160 161 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged"; 162 163 public static final String EXTRA_RESULT_CODE = "resultCode"; 164 public static final String EXTRA_RESULT_COUNT = "count"; 165 166 public static final int CP2_ERROR = 0; 167 public static final int CONTACTS_LINKED = 1; 168 public static final int CONTACTS_SPLIT = 2; 169 public static final int BAD_ARGUMENTS = 3; 170 public static final int RESULT_UNKNOWN = 0; 171 public static final int RESULT_SUCCESS = 1; 172 public static final int RESULT_FAILURE = 2; 173 174 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet( 175 Data.MIMETYPE, 176 Data.IS_PRIMARY, 177 Data.DATA1, 178 Data.DATA2, 179 Data.DATA3, 180 Data.DATA4, 181 Data.DATA5, 182 Data.DATA6, 183 Data.DATA7, 184 Data.DATA8, 185 Data.DATA9, 186 Data.DATA10, 187 Data.DATA11, 188 Data.DATA12, 189 Data.DATA13, 190 Data.DATA14, 191 Data.DATA15 192 ); 193 194 private static final int PERSIST_TRIES = 3; 195 196 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499; 197 198 public interface Listener { onServiceCompleted(Intent callbackIntent)199 public void onServiceCompleted(Intent callbackIntent); 200 } 201 202 private static final CopyOnWriteArrayList<Listener> sListeners = 203 new CopyOnWriteArrayList<Listener>(); 204 205 // Holds the current state of the service 206 private static final State sState = new State(); 207 208 private Handler mMainHandler; 209 private GroupsDao mGroupsDao; 210 private SimContactDao mSimContactDao; 211 ContactSaveService()212 public ContactSaveService() { 213 super(TAG); 214 setIntentRedelivery(true); 215 mMainHandler = new Handler(Looper.getMainLooper()); 216 } 217 218 @Override onCreate()219 public void onCreate() { 220 super.onCreate(); 221 mGroupsDao = new GroupsDaoImpl(this); 222 mSimContactDao = SimContactDao.create(this); 223 } 224 registerListener(Listener listener)225 public static void registerListener(Listener listener) { 226 if (!(listener instanceof Activity)) { 227 throw new ClassCastException("Only activities can be registered to" 228 + " receive callback from " + ContactSaveService.class.getName()); 229 } 230 sListeners.add(0, listener); 231 } 232 canUndo(Intent resultIntent)233 public static boolean canUndo(Intent resultIntent) { 234 return resultIntent.hasExtra(EXTRA_UNDO_DATA); 235 } 236 unregisterListener(Listener listener)237 public static void unregisterListener(Listener listener) { 238 sListeners.remove(listener); 239 } 240 getState()241 public static State getState() { 242 return sState; 243 } 244 notifyStateChanged()245 private void notifyStateChanged() { 246 LocalBroadcastManager.getInstance(this) 247 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED)); 248 } 249 250 /** 251 * Returns true if the ContactSaveService was started successfully and false if an exception 252 * was thrown and a Toast error message was displayed. 253 */ startService(Context context, Intent intent, int saveMode)254 public static boolean startService(Context context, Intent intent, int saveMode) { 255 try { 256 context.startService(intent); 257 } catch (Exception exception) { 258 final int resId; 259 switch (saveMode) { 260 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT: 261 resId = R.string.contactUnlinkErrorToast; 262 break; 263 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD: 264 resId = R.string.contactJoinErrorToast; 265 break; 266 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE: 267 resId = R.string.contactSavedErrorToast; 268 break; 269 default: 270 resId = R.string.contactGenericErrorToast; 271 } 272 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show(); 273 return false; 274 } 275 return true; 276 } 277 278 /** 279 * Utility method that starts service and handles exception. 280 */ startService(Context context, Intent intent)281 public static void startService(Context context, Intent intent) { 282 try { 283 context.startService(intent); 284 } catch (Exception exception) { 285 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show(); 286 } 287 } 288 289 @Override getSystemService(String name)290 public Object getSystemService(String name) { 291 Object service = super.getSystemService(name); 292 if (service != null) { 293 return service; 294 } 295 296 return getApplicationContext().getSystemService(name); 297 } 298 299 // Parent classes Javadoc says not to override this method but we're doing it just to update 300 // our state which should be OK since we're still doing the work in onHandleIntent 301 @Override onStartCommand(Intent intent, int flags, int startId)302 public int onStartCommand(Intent intent, int flags, int startId) { 303 sState.onStart(intent); 304 notifyStateChanged(); 305 return super.onStartCommand(intent, flags, startId); 306 } 307 308 @Override onHandleIntent(final Intent intent)309 protected void onHandleIntent(final Intent intent) { 310 if (intent == null) { 311 if (Log.isLoggable(TAG, Log.DEBUG)) { 312 Log.d(TAG, "onHandleIntent: could not handle null intent"); 313 } 314 return; 315 } 316 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) { 317 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2"); 318 // TODO: add more specific error string such as "Turn on Contacts 319 // permission to update your contacts" 320 showToast(R.string.contactSavedErrorToast); 321 return; 322 } 323 324 // Call an appropriate method. If we're sure it affects how incoming phone calls are 325 // handled, then notify the fact to in-call screen. 326 String action = intent.getAction(); 327 if (ACTION_NEW_RAW_CONTACT.equals(action)) { 328 createRawContact(intent); 329 } else if (ACTION_SAVE_CONTACT.equals(action)) { 330 saveContact(intent); 331 } else if (ACTION_CREATE_GROUP.equals(action)) { 332 createGroup(intent); 333 } else if (ACTION_RENAME_GROUP.equals(action)) { 334 renameGroup(intent); 335 } else if (ACTION_DELETE_GROUP.equals(action)) { 336 deleteGroup(intent); 337 } else if (ACTION_UPDATE_GROUP.equals(action)) { 338 updateGroup(intent); 339 } else if (ACTION_SET_STARRED.equals(action)) { 340 setStarred(intent); 341 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) { 342 setSuperPrimary(intent); 343 } else if (ACTION_CLEAR_PRIMARY.equals(action)) { 344 clearPrimary(intent); 345 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) { 346 deleteMultipleContacts(intent); 347 } else if (ACTION_DELETE_CONTACT.equals(action)) { 348 deleteContact(intent); 349 } else if (ACTION_SPLIT_CONTACT.equals(action)) { 350 splitContact(intent); 351 } else if (ACTION_JOIN_CONTACTS.equals(action)) { 352 joinContacts(intent); 353 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) { 354 joinSeveralContacts(intent); 355 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) { 356 setSendToVoicemail(intent); 357 } else if (ACTION_SET_RINGTONE.equals(action)) { 358 setRingtone(intent); 359 } else if (ACTION_UNDO.equals(action)) { 360 undo(intent); 361 } else if (ACTION_SLEEP.equals(action)) { 362 sleepForDebugging(intent); 363 } 364 365 sState.onFinish(intent); 366 notifyStateChanged(); 367 } 368 369 /** 370 * Creates an intent that can be sent to this service to create a new raw contact 371 * using data presented as a set of ContentValues. 372 */ createNewRawContactIntent(Context context, ArrayList<ContentValues> values, AccountWithDataSet account, Class<? extends Activity> callbackActivity, String callbackAction)373 public static Intent createNewRawContactIntent(Context context, 374 ArrayList<ContentValues> values, AccountWithDataSet account, 375 Class<? extends Activity> callbackActivity, String callbackAction) { 376 Intent serviceIntent = new Intent( 377 context, ContactSaveService.class); 378 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT); 379 if (account != null) { 380 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name); 381 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type); 382 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet); 383 } 384 serviceIntent.putParcelableArrayListExtra( 385 ContactSaveService.EXTRA_CONTENT_VALUES, values); 386 387 // Callback intent will be invoked by the service once the new contact is 388 // created. The service will put the URI of the new contact as "data" on 389 // the callback intent. 390 Intent callbackIntent = new Intent(context, callbackActivity); 391 callbackIntent.setAction(callbackAction); 392 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 393 return serviceIntent; 394 } 395 createRawContact(Intent intent)396 private void createRawContact(Intent intent) { 397 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); 398 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); 399 String dataSet = intent.getStringExtra(EXTRA_DATA_SET); 400 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES); 401 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 402 403 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 404 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) 405 .withValue(RawContacts.ACCOUNT_NAME, accountName) 406 .withValue(RawContacts.ACCOUNT_TYPE, accountType) 407 .withValue(RawContacts.DATA_SET, dataSet) 408 .build()); 409 410 int size = valueList.size(); 411 for (int i = 0; i < size; i++) { 412 ContentValues values = valueList.get(i); 413 values.keySet().retainAll(ALLOWED_DATA_COLUMNS); 414 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI) 415 .withValueBackReference(Data.RAW_CONTACT_ID, 0) 416 .withValues(values) 417 .build()); 418 } 419 420 ContentResolver resolver = getContentResolver(); 421 ContentProviderResult[] results; 422 try { 423 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations); 424 } catch (Exception e) { 425 throw new RuntimeException("Failed to store new contact", e); 426 } 427 428 Uri rawContactUri = results[0].uri; 429 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri)); 430 431 deliverCallback(callbackIntent); 432 } 433 434 /** 435 * Creates an intent that can be sent to this service to create a new raw contact 436 * using data presented as a set of ContentValues. 437 * This variant is more convenient to use when there is only one photo that can 438 * possibly be updated, as in the Contact Details screen. 439 * @param rawContactId identifies a writable raw-contact whose photo is to be updated. 440 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo. 441 */ createSaveContactIntent(Context context, RawContactDeltaList state, String saveModeExtraKey, int saveMode, boolean isProfile, Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId, Uri updatedPhotoPath)442 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state, 443 String saveModeExtraKey, int saveMode, boolean isProfile, 444 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId, 445 Uri updatedPhotoPath) { 446 Bundle bundle = new Bundle(); 447 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath); 448 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile, 449 callbackActivity, callbackAction, bundle, 450 /* joinContactIdExtraKey */ null, /* joinContactId */ null); 451 } 452 453 /** 454 * Creates an intent that can be sent to this service to create a new raw contact 455 * using data presented as a set of ContentValues. 456 * This variant is used when multiple contacts' photos may be updated, as in the 457 * Contact Editor. 458 * 459 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo. 460 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent. 461 * @param joinContactId the raw contact ID to join to the contact after doing the save. 462 */ createSaveContactIntent(Context context, RawContactDeltaList state, String saveModeExtraKey, int saveMode, boolean isProfile, Class<? extends Activity> callbackActivity, String callbackAction, Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId)463 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state, 464 String saveModeExtraKey, int saveMode, boolean isProfile, 465 Class<? extends Activity> callbackActivity, String callbackAction, 466 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) { 467 Intent serviceIntent = new Intent( 468 context, ContactSaveService.class); 469 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT); 470 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state); 471 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile); 472 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode); 473 474 if (updatedPhotos != null) { 475 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos); 476 } 477 478 if (callbackActivity != null) { 479 // Callback intent will be invoked by the service once the contact is 480 // saved. The service will put the URI of the new contact as "data" on 481 // the callback intent. 482 Intent callbackIntent = new Intent(context, callbackActivity); 483 callbackIntent.putExtra(saveModeExtraKey, saveMode); 484 if (joinContactIdExtraKey != null && joinContactId != null) { 485 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId); 486 } 487 callbackIntent.setAction(callbackAction); 488 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 489 } 490 return serviceIntent; 491 } 492 saveContact(Intent intent)493 private void saveContact(Intent intent) { 494 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE); 495 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false); 496 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS); 497 498 if (state == null) { 499 Log.e(TAG, "Invalid arguments for saveContact request"); 500 return; 501 } 502 503 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1); 504 // Trim any empty fields, and RawContacts, before persisting 505 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this); 506 RawContactModifier.trimEmpty(state, accountTypes); 507 508 Uri lookupUri = null; 509 510 final ContentResolver resolver = getContentResolver(); 511 512 boolean succeeded = false; 513 514 // Keep track of the id of a newly raw-contact (if any... there can be at most one). 515 long insertedRawContactId = -1; 516 517 // Attempt to persist changes 518 int tries = 0; 519 while (tries++ < PERSIST_TRIES) { 520 try { 521 // Build operations and try applying 522 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper(); 523 524 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList(); 525 526 for (CPOWrapper cpoWrapper : diffWrapper) { 527 diff.add(cpoWrapper.getOperation()); 528 } 529 530 if (DEBUG) { 531 Log.v(TAG, "Content Provider Operations:"); 532 for (ContentProviderOperation operation : diff) { 533 Log.v(TAG, operation.toString()); 534 } 535 } 536 537 int numberProcessed = 0; 538 boolean batchFailed = false; 539 final ContentProviderResult[] results = new ContentProviderResult[diff.size()]; 540 while (numberProcessed < diff.size()) { 541 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver); 542 if (subsetCount == -1) { 543 Log.w(TAG, "Resolver.applyBatch failed in saveContacts"); 544 batchFailed = true; 545 break; 546 } else { 547 numberProcessed += subsetCount; 548 } 549 } 550 551 if (batchFailed) { 552 // Retry save 553 continue; 554 } 555 556 final long rawContactId = getRawContactId(state, diffWrapper, results); 557 if (rawContactId == -1) { 558 throw new IllegalStateException("Could not determine RawContact ID after save"); 559 } 560 // We don't have to check to see if the value is still -1. If we reach here, 561 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus. 562 insertedRawContactId = getInsertedRawContactId(diffWrapper, results); 563 if (isProfile) { 564 // Since the profile supports local raw contacts, which may have been completely 565 // removed if all information was removed, we need to do a special query to 566 // get the lookup URI for the profile contact (if it still exists). 567 Cursor c = resolver.query(Profile.CONTENT_URI, 568 new String[] {Contacts._ID, Contacts.LOOKUP_KEY}, 569 null, null, null); 570 if (c == null) { 571 continue; 572 } 573 try { 574 if (c.moveToFirst()) { 575 final long contactId = c.getLong(0); 576 final String lookupKey = c.getString(1); 577 lookupUri = Contacts.getLookupUri(contactId, lookupKey); 578 } 579 } finally { 580 c.close(); 581 } 582 } else { 583 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, 584 rawContactId); 585 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri); 586 } 587 if (lookupUri != null && Log.isLoggable(TAG, Log.VERBOSE)) { 588 Log.v(TAG, "Saved contact. New URI: " + lookupUri); 589 } 590 591 // We can change this back to false later, if we fail to save the contact photo. 592 succeeded = true; 593 break; 594 595 } catch (RemoteException e) { 596 // Something went wrong, bail without success 597 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e); 598 break; 599 600 } catch (IllegalArgumentException e) { 601 // This is thrown by applyBatch on malformed requests 602 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e); 603 showToast(R.string.contactSavedErrorToast); 604 break; 605 606 } catch (OperationApplicationException e) { 607 // Version consistency failed, re-parent change and try again 608 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString()); 609 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN("); 610 boolean first = true; 611 final int count = state.size(); 612 for (int i = 0; i < count; i++) { 613 Long rawContactId = state.getRawContactId(i); 614 if (rawContactId != null && rawContactId != -1) { 615 if (!first) { 616 sb.append(','); 617 } 618 sb.append(rawContactId); 619 first = false; 620 } 621 } 622 sb.append(")"); 623 624 if (first) { 625 throw new IllegalStateException( 626 "Version consistency failed for a new contact", e); 627 } 628 629 final RawContactDeltaList newState = RawContactDeltaList.fromQuery( 630 isProfile 631 ? RawContactsEntity.PROFILE_CONTENT_URI 632 : RawContactsEntity.CONTENT_URI, 633 resolver, sb.toString(), null, null); 634 state = RawContactDeltaList.mergeAfter(newState, state); 635 636 // Update the new state to use profile URIs if appropriate. 637 if (isProfile) { 638 for (RawContactDelta delta : state) { 639 delta.setProfileQueryUri(); 640 } 641 } 642 } 643 } 644 645 // Now save any updated photos. We do this at the end to ensure that 646 // the ContactProvider already knows about newly-created contacts. 647 if (updatedPhotos != null) { 648 for (String key : updatedPhotos.keySet()) { 649 Uri photoUri = updatedPhotos.getParcelable(key); 650 long rawContactId = Long.parseLong(key); 651 652 // If the raw-contact ID is negative, we are saving a new raw-contact; 653 // replace the bogus ID with the new one that we actually saved the contact at. 654 if (rawContactId < 0) { 655 rawContactId = insertedRawContactId; 656 } 657 658 // If the save failed, insertedRawContactId will be -1 659 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) { 660 succeeded = false; 661 } 662 } 663 } 664 665 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 666 if (callbackIntent != null) { 667 if (succeeded) { 668 // Mark the intent to indicate that the save was successful (even if the lookup URI 669 // is now null). For local contacts or the local profile, it's possible that the 670 // save triggered removal of the contact, so no lookup URI would exist.. 671 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true); 672 } 673 callbackIntent.setData(lookupUri); 674 deliverCallback(callbackIntent); 675 } 676 } 677 678 /** 679 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the 680 * subsets, adds the returned array to "results". 681 * 682 * @return the size of the array, if not null; -1 when the array is null. 683 */ applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset, ContentProviderResult[] results, ContentResolver resolver)684 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset, 685 ContentProviderResult[] results, ContentResolver resolver) 686 throws RemoteException, OperationApplicationException { 687 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE); 688 final ArrayList<ContentProviderOperation> subset = new ArrayList<>(); 689 subset.addAll(diff.subList(offset, offset + subsetCount)); 690 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract 691 .AUTHORITY, subset); 692 if (subsetResult == null || (offset + subsetResult.length) > results.length) { 693 return -1; 694 } 695 for (ContentProviderResult c : subsetResult) { 696 results[offset++] = c; 697 } 698 return subsetResult.length; 699 } 700 701 /** 702 * Save updated photo for the specified raw-contact. 703 * @return true for success, false for failure 704 */ saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode)705 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) { 706 final Uri outputUri = Uri.withAppendedPath( 707 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 708 RawContacts.DisplayPhoto.CONTENT_DIRECTORY); 709 710 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0)); 711 } 712 713 /** 714 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1. 715 */ getRawContactId(RawContactDeltaList state, final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results)716 private long getRawContactId(RawContactDeltaList state, 717 final ArrayList<CPOWrapper> diffWrapper, 718 final ContentProviderResult[] results) { 719 long existingRawContactId = state.findRawContactId(); 720 if (existingRawContactId != -1) { 721 return existingRawContactId; 722 } 723 724 return getInsertedRawContactId(diffWrapper, results); 725 } 726 727 /** 728 * Find the ID of a newly-inserted raw-contact. If none exists, return -1. 729 */ getInsertedRawContactId( final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results)730 private long getInsertedRawContactId( 731 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) { 732 if (results == null) { 733 return -1; 734 } 735 final int diffSize = diffWrapper.size(); 736 final int numResults = results.length; 737 for (int i = 0; i < diffSize && i < numResults; i++) { 738 final CPOWrapper cpoWrapper = diffWrapper.get(i); 739 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper); 740 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains( 741 RawContacts.CONTENT_URI.getEncodedPath())) { 742 return ContentUris.parseId(results[i].uri); 743 } 744 } 745 return -1; 746 } 747 748 /** 749 * Creates an intent that can be sent to this service to create a new group as 750 * well as add new members at the same time. 751 * 752 * @param context of the application 753 * @param account in which the group should be created 754 * @param label is the name of the group (cannot be null) 755 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 756 * should be added to the group 757 * @param callbackActivity is the activity to send the callback intent to 758 * @param callbackAction is the intent action for the callback intent 759 */ createNewGroupIntent(Context context, AccountWithDataSet account, String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity, String callbackAction)760 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account, 761 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity, 762 String callbackAction) { 763 Intent serviceIntent = new Intent(context, ContactSaveService.class); 764 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP); 765 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type); 766 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name); 767 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet); 768 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label); 769 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 770 771 // Callback intent will be invoked by the service once the new group is 772 // created. 773 Intent callbackIntent = new Intent(context, callbackActivity); 774 callbackIntent.setAction(callbackAction); 775 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 776 777 return serviceIntent; 778 } 779 createGroup(Intent intent)780 private void createGroup(Intent intent) { 781 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); 782 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); 783 String dataSet = intent.getStringExtra(EXTRA_DATA_SET); 784 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 785 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 786 787 // Create the new group 788 final Uri groupUri = mGroupsDao.create(label, 789 new AccountWithDataSet(accountName, accountType, dataSet)); 790 final ContentResolver resolver = getContentResolver(); 791 792 // If there's no URI, then the insertion failed. Abort early because group members can't be 793 // added if the group doesn't exist 794 if (groupUri == null) { 795 Log.e(TAG, "Couldn't create group with label " + label); 796 return; 797 } 798 799 // Add new group members 800 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri)); 801 802 ContentValues values = new ContentValues(); 803 // TODO: Move this into the contact editor where it belongs. This needs to be integrated 804 // with the way other intent extras that are passed to the 805 // {@link ContactEditorActivity}. 806 values.clear(); 807 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 808 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri)); 809 810 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 811 callbackIntent.setData(groupUri); 812 // TODO: This can be taken out when the above TODO is addressed 813 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values)); 814 deliverCallback(callbackIntent); 815 } 816 817 /** 818 * Creates an intent that can be sent to this service to rename a group. 819 */ createGroupRenameIntent(Context context, long groupId, String newLabel, Class<? extends Activity> callbackActivity, String callbackAction)820 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel, 821 Class<? extends Activity> callbackActivity, String callbackAction) { 822 Intent serviceIntent = new Intent(context, ContactSaveService.class); 823 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP); 824 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 825 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 826 827 // Callback intent will be invoked by the service once the group is renamed. 828 Intent callbackIntent = new Intent(context, callbackActivity); 829 callbackIntent.setAction(callbackAction); 830 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 831 832 return serviceIntent; 833 } 834 renameGroup(Intent intent)835 private void renameGroup(Intent intent) { 836 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 837 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 838 839 if (groupId == -1) { 840 Log.e(TAG, "Invalid arguments for renameGroup request"); 841 return; 842 } 843 844 ContentValues values = new ContentValues(); 845 values.put(Groups.TITLE, label); 846 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 847 getContentResolver().update(groupUri, values, null, null); 848 849 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 850 callbackIntent.setData(groupUri); 851 deliverCallback(callbackIntent); 852 } 853 854 /** 855 * Creates an intent that can be sent to this service to delete a group. 856 */ createGroupDeletionIntent(Context context, long groupId)857 public static Intent createGroupDeletionIntent(Context context, long groupId) { 858 final Intent serviceIntent = new Intent(context, ContactSaveService.class); 859 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP); 860 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 861 862 return serviceIntent; 863 } 864 deleteGroup(Intent intent)865 private void deleteGroup(Intent intent) { 866 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 867 if (groupId == -1) { 868 Log.e(TAG, "Invalid arguments for deleteGroup request"); 869 return; 870 } 871 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 872 873 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED); 874 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri); 875 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP); 876 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData); 877 878 mGroupsDao.delete(groupUri); 879 880 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent); 881 } 882 createUndoIntent(Context context, Intent resultIntent)883 public static Intent createUndoIntent(Context context, Intent resultIntent) { 884 final Intent serviceIntent = new Intent(context, ContactSaveService.class); 885 serviceIntent.setAction(ContactSaveService.ACTION_UNDO); 886 serviceIntent.putExtras(resultIntent); 887 return serviceIntent; 888 } 889 undo(Intent intent)890 private void undo(Intent intent) { 891 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION); 892 if (ACTION_DELETE_GROUP.equals(actionToUndo)) { 893 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA)); 894 } 895 } 896 897 898 /** 899 * Creates an intent that can be sent to this service to rename a group as 900 * well as add and remove members from the group. 901 * 902 * @param context of the application 903 * @param groupId of the group that should be modified 904 * @param newLabel is the updated name of the group (can be null if the name 905 * should not be updated) 906 * @param rawContactsToAdd is an array of raw contact IDs for contacts that 907 * should be added to the group 908 * @param rawContactsToRemove is an array of raw contact IDs for contacts 909 * that should be removed from the group 910 * @param callbackActivity is the activity to send the callback intent to 911 * @param callbackAction is the intent action for the callback intent 912 */ createGroupUpdateIntent(Context context, long groupId, String newLabel, long[] rawContactsToAdd, long[] rawContactsToRemove, Class<? extends Activity> callbackActivity, String callbackAction)913 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel, 914 long[] rawContactsToAdd, long[] rawContactsToRemove, 915 Class<? extends Activity> callbackActivity, String callbackAction) { 916 Intent serviceIntent = new Intent(context, ContactSaveService.class); 917 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP); 918 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId); 919 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel); 920 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd); 921 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE, 922 rawContactsToRemove); 923 924 // Callback intent will be invoked by the service once the group is updated 925 Intent callbackIntent = new Intent(context, callbackActivity); 926 callbackIntent.setAction(callbackAction); 927 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 928 929 return serviceIntent; 930 } 931 updateGroup(Intent intent)932 private void updateGroup(Intent intent) { 933 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1); 934 String label = intent.getStringExtra(EXTRA_GROUP_LABEL); 935 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD); 936 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE); 937 938 if (groupId == -1) { 939 Log.e(TAG, "Invalid arguments for updateGroup request"); 940 return; 941 } 942 943 final ContentResolver resolver = getContentResolver(); 944 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId); 945 946 // Update group name if necessary 947 if (label != null) { 948 ContentValues values = new ContentValues(); 949 values.put(Groups.TITLE, label); 950 resolver.update(groupUri, values, null, null); 951 } 952 953 // Add and remove members if necessary 954 addMembersToGroup(resolver, rawContactsToAdd, groupId); 955 removeMembersFromGroup(resolver, rawContactsToRemove, groupId); 956 957 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 958 callbackIntent.setData(groupUri); 959 deliverCallback(callbackIntent); 960 } 961 addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd, long groupId)962 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd, 963 long groupId) { 964 if (rawContactsToAdd == null) { 965 return; 966 } 967 for (long rawContactId : rawContactsToAdd) { 968 try { 969 final ArrayList<ContentProviderOperation> rawContactOperations = 970 new ArrayList<ContentProviderOperation>(); 971 972 // Build an assert operation to ensure the contact is not already in the group 973 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation 974 .newAssertQuery(Data.CONTENT_URI); 975 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " + 976 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 977 new String[] { String.valueOf(rawContactId), 978 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 979 assertBuilder.withExpectedCount(0); 980 rawContactOperations.add(assertBuilder.build()); 981 982 // Build an insert operation to add the contact to the group 983 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation 984 .newInsert(Data.CONTENT_URI); 985 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId); 986 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 987 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId); 988 rawContactOperations.add(insertBuilder.build()); 989 990 if (DEBUG) { 991 for (ContentProviderOperation operation : rawContactOperations) { 992 Log.v(TAG, operation.toString()); 993 } 994 } 995 996 // Apply batch 997 if (!rawContactOperations.isEmpty()) { 998 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations); 999 } 1000 } catch (RemoteException e) { 1001 // Something went wrong, bail without success 1002 FeedbackHelper.sendFeedback(this, TAG, 1003 "Problem persisting user edits for raw contact ID " + 1004 String.valueOf(rawContactId), e); 1005 } catch (OperationApplicationException e) { 1006 // The assert could have failed because the contact is already in the group, 1007 // just continue to the next contact 1008 FeedbackHelper.sendFeedback(this, TAG, 1009 "Assert failed in adding raw contact ID " + 1010 String.valueOf(rawContactId) + ". Already exists in group " + 1011 String.valueOf(groupId), e); 1012 } 1013 } 1014 } 1015 removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove, long groupId)1016 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove, 1017 long groupId) { 1018 if (rawContactsToRemove == null) { 1019 return; 1020 } 1021 for (long rawContactId : rawContactsToRemove) { 1022 // Apply the delete operation on the data row for the given raw contact's 1023 // membership in the given group. If no contact matches the provided selection, then 1024 // nothing will be done. Just continue to the next contact. 1025 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " + 1026 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 1027 new String[] { String.valueOf(rawContactId), 1028 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)}); 1029 } 1030 } 1031 1032 /** 1033 * Creates an intent that can be sent to this service to star or un-star a contact. 1034 */ createSetStarredIntent(Context context, Uri contactUri, boolean value)1035 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) { 1036 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1037 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED); 1038 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 1039 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value); 1040 1041 return serviceIntent; 1042 } 1043 setStarred(Intent intent)1044 private void setStarred(Intent intent) { 1045 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1046 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false); 1047 if (contactUri == null) { 1048 Log.e(TAG, "Invalid arguments for setStarred request"); 1049 return; 1050 } 1051 1052 final ContentValues values = new ContentValues(1); 1053 values.put(Contacts.STARRED, value); 1054 getContentResolver().update(contactUri, values, null, null); 1055 1056 // Undemote the contact if necessary 1057 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID}, 1058 null, null, null); 1059 if (c == null) { 1060 return; 1061 } 1062 try { 1063 if (c.moveToFirst()) { 1064 final long id = c.getLong(0); 1065 1066 // Don't bother undemoting if this contact is the user's profile. 1067 if (id < Profile.MIN_ID) { 1068 PinnedPositionsCompat.undemote(getContentResolver(), id); 1069 } 1070 } 1071 } finally { 1072 c.close(); 1073 } 1074 } 1075 1076 /** 1077 * Creates an intent that can be sent to this service to set the redirect to voicemail. 1078 */ createSetSendToVoicemail(Context context, Uri contactUri, boolean value)1079 public static Intent createSetSendToVoicemail(Context context, Uri contactUri, 1080 boolean value) { 1081 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1082 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL); 1083 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 1084 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value); 1085 1086 return serviceIntent; 1087 } 1088 setSendToVoicemail(Intent intent)1089 private void setSendToVoicemail(Intent intent) { 1090 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1091 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false); 1092 if (contactUri == null) { 1093 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail"); 1094 return; 1095 } 1096 1097 final ContentValues values = new ContentValues(1); 1098 values.put(Contacts.SEND_TO_VOICEMAIL, value); 1099 getContentResolver().update(contactUri, values, null, null); 1100 } 1101 1102 /** 1103 * Creates an intent that can be sent to this service to save the contact's ringtone. 1104 */ createSetRingtone(Context context, Uri contactUri, String value)1105 public static Intent createSetRingtone(Context context, Uri contactUri, 1106 String value) { 1107 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1108 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE); 1109 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 1110 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value); 1111 1112 return serviceIntent; 1113 } 1114 setRingtone(Intent intent)1115 private void setRingtone(Intent intent) { 1116 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1117 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE); 1118 if (contactUri == null) { 1119 Log.e(TAG, "Invalid arguments for setRingtone"); 1120 return; 1121 } 1122 ContentValues values = new ContentValues(1); 1123 values.put(Contacts.CUSTOM_RINGTONE, value); 1124 getContentResolver().update(contactUri, values, null, null); 1125 } 1126 1127 /** 1128 * Creates an intent that sets the selected data item as super primary (default) 1129 */ createSetSuperPrimaryIntent(Context context, long dataId)1130 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) { 1131 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1132 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY); 1133 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 1134 return serviceIntent; 1135 } 1136 setSuperPrimary(Intent intent)1137 private void setSuperPrimary(Intent intent) { 1138 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 1139 if (dataId == -1) { 1140 Log.e(TAG, "Invalid arguments for setSuperPrimary request"); 1141 return; 1142 } 1143 1144 ContactUpdateUtils.setSuperPrimary(this, dataId); 1145 } 1146 1147 /** 1148 * Creates an intent that clears the primary flag of all data items that belong to the same 1149 * raw_contact as the given data item. Will only clear, if the data item was primary before 1150 * this call 1151 */ createClearPrimaryIntent(Context context, long dataId)1152 public static Intent createClearPrimaryIntent(Context context, long dataId) { 1153 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1154 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY); 1155 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId); 1156 return serviceIntent; 1157 } 1158 clearPrimary(Intent intent)1159 private void clearPrimary(Intent intent) { 1160 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1); 1161 if (dataId == -1) { 1162 Log.e(TAG, "Invalid arguments for clearPrimary request"); 1163 return; 1164 } 1165 1166 // Update the primary values in the data record. 1167 ContentValues values = new ContentValues(1); 1168 values.put(Data.IS_SUPER_PRIMARY, 0); 1169 values.put(Data.IS_PRIMARY, 0); 1170 1171 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), 1172 values, null, null); 1173 } 1174 1175 /** 1176 * Creates an intent that can be sent to this service to delete a contact. 1177 */ createDeleteContactIntent(Context context, Uri contactUri)1178 public static Intent createDeleteContactIntent(Context context, Uri contactUri) { 1179 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1180 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT); 1181 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri); 1182 return serviceIntent; 1183 } 1184 1185 /** 1186 * Creates an intent that can be sent to this service to delete multiple contacts. 1187 */ createDeleteMultipleContactsIntent(Context context, long[] contactIds, final String[] names)1188 public static Intent createDeleteMultipleContactsIntent(Context context, 1189 long[] contactIds, final String[] names) { 1190 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1191 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS); 1192 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds); 1193 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names); 1194 return serviceIntent; 1195 } 1196 deleteContact(Intent intent)1197 private void deleteContact(Intent intent) { 1198 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI); 1199 if (contactUri == null) { 1200 Log.e(TAG, "Invalid arguments for deleteContact request"); 1201 return; 1202 } 1203 1204 getContentResolver().delete(contactUri, null, null); 1205 } 1206 deleteMultipleContacts(Intent intent)1207 private void deleteMultipleContacts(Intent intent) { 1208 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS); 1209 if (contactIds == null) { 1210 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request"); 1211 return; 1212 } 1213 for (long contactId : contactIds) { 1214 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 1215 getContentResolver().delete(contactUri, null, null); 1216 } 1217 final String[] names = intent.getStringArrayExtra( 1218 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY); 1219 final String deleteToastMessage; 1220 if (contactIds.length != names.length || names.length == 0) { 1221 MessageFormat msgFormat = new MessageFormat( 1222 getResources().getString(R.string.contacts_deleted_toast), 1223 Locale.getDefault()); 1224 Map<String, Object> arguments = new HashMap<>(); 1225 arguments.put("count", contactIds.length); 1226 deleteToastMessage = msgFormat.format(arguments); 1227 } else if (names.length == 1) { 1228 deleteToastMessage = getResources().getString( 1229 R.string.contacts_deleted_one_named_toast, (Object[]) names); 1230 } else if (names.length == 2) { 1231 deleteToastMessage = getResources().getString( 1232 R.string.contacts_deleted_two_named_toast, (Object[]) names); 1233 } else { 1234 deleteToastMessage = getResources().getString( 1235 R.string.contacts_deleted_many_named_toast, (Object[]) names); 1236 } 1237 1238 mMainHandler.post(new Runnable() { 1239 @Override 1240 public void run() { 1241 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG) 1242 .show(); 1243 } 1244 }); 1245 } 1246 1247 /** 1248 * Creates an intent that can be sent to this service to split a contact into it's constituent 1249 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so 1250 * they may be re-merged by the auto-aggregator. 1251 */ createSplitContactIntent(Context context, long[][] rawContactIds, ResultReceiver receiver)1252 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds, 1253 ResultReceiver receiver) { 1254 final Intent serviceIntent = new Intent(context, ContactSaveService.class); 1255 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT); 1256 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds); 1257 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver); 1258 return serviceIntent; 1259 } 1260 1261 /** 1262 * Creates an intent that can be sent to this service to split a contact into it's constituent 1263 * pieces. This will explicitly set the raw contact ids to 1264 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}. 1265 */ createHardSplitContactIntent(Context context, long[][] rawContactIds)1266 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) { 1267 final Intent serviceIntent = new Intent(context, ContactSaveService.class); 1268 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT); 1269 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds); 1270 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true); 1271 return serviceIntent; 1272 } 1273 splitContact(Intent intent)1274 private void splitContact(Intent intent) { 1275 final long rawContactIds[][] = (long[][]) intent 1276 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS); 1277 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); 1278 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false); 1279 if (rawContactIds == null) { 1280 Log.e(TAG, "Invalid argument for splitContact request"); 1281 if (receiver != null) { 1282 receiver.send(BAD_ARGUMENTS, new Bundle()); 1283 } 1284 return; 1285 } 1286 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE; 1287 final ContentResolver resolver = getContentResolver(); 1288 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize); 1289 for (int i = 0; i < rawContactIds.length; i++) { 1290 for (int j = 0; j < rawContactIds.length; j++) { 1291 if (i != j) { 1292 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j], 1293 hardSplit)) { 1294 if (receiver != null) { 1295 receiver.send(CP2_ERROR, new Bundle()); 1296 return; 1297 } 1298 } 1299 } 1300 } 1301 } 1302 if (operations.size() > 0 && !applyOperations(resolver, operations)) { 1303 if (receiver != null) { 1304 receiver.send(CP2_ERROR, new Bundle()); 1305 } 1306 return; 1307 } 1308 LocalBroadcastManager.getInstance(this) 1309 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE)); 1310 if (receiver != null) { 1311 receiver.send(CONTACTS_SPLIT, new Bundle()); 1312 } else { 1313 showToast(R.string.contactUnlinkedToast); 1314 } 1315 } 1316 1317 /** 1318 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1} 1319 * and {@param rawContactIds2} to {@param operations}. 1320 * @return false if an error occurred, true otherwise. 1321 */ buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations, long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit)1322 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations, 1323 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) { 1324 if (rawContactIds1 == null || rawContactIds2 == null) { 1325 Log.e(TAG, "Invalid arguments for splitContact request"); 1326 return false; 1327 } 1328 // For each pair of raw contacts, insert an aggregation exception 1329 final ContentResolver resolver = getContentResolver(); 1330 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225 1331 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE; 1332 for (int i = 0; i < rawContactIds1.length; i++) { 1333 for (int j = 0; j < rawContactIds2.length; j++) { 1334 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit); 1335 // Before we get to 500 we need to flush the operations list 1336 if (operations.size() > 0 && operations.size() % batchSize == 0) { 1337 if (!applyOperations(resolver, operations)) { 1338 return false; 1339 } 1340 operations.clear(); 1341 } 1342 } 1343 } 1344 return true; 1345 } 1346 1347 /** 1348 * Creates an intent that can be sent to this service to join two contacts. 1349 * The resulting contact uses the name from {@param contactId1} if possible. 1350 */ createJoinContactsIntent(Context context, long contactId1, long contactId2, Class<? extends Activity> callbackActivity, String callbackAction)1351 public static Intent createJoinContactsIntent(Context context, long contactId1, 1352 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) { 1353 Intent serviceIntent = new Intent(context, ContactSaveService.class); 1354 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS); 1355 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1); 1356 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2); 1357 1358 // Callback intent will be invoked by the service once the contacts are joined. 1359 Intent callbackIntent = new Intent(context, callbackActivity); 1360 callbackIntent.setAction(callbackAction); 1361 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent); 1362 1363 return serviceIntent; 1364 } 1365 1366 /** 1367 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts. 1368 * No special attention is paid to where the resulting contact's name is taken from. 1369 */ createJoinSeveralContactsIntent(Context context, long[] contactIds, ResultReceiver receiver)1370 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds, 1371 ResultReceiver receiver) { 1372 final Intent serviceIntent = new Intent(context, ContactSaveService.class); 1373 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS); 1374 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds); 1375 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver); 1376 return serviceIntent; 1377 } 1378 1379 /** 1380 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts. 1381 * No special attention is paid to where the resulting contact's name is taken from. 1382 */ createJoinSeveralContactsIntent(Context context, long[] contactIds)1383 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) { 1384 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null); 1385 } 1386 1387 private interface JoinContactQuery { 1388 String[] PROJECTION = { 1389 RawContacts._ID, 1390 RawContacts.CONTACT_ID, 1391 RawContacts.DISPLAY_NAME_SOURCE, 1392 }; 1393 1394 int _ID = 0; 1395 int CONTACT_ID = 1; 1396 int DISPLAY_NAME_SOURCE = 2; 1397 } 1398 1399 private interface ContactEntityQuery { 1400 String[] PROJECTION = { 1401 Contacts.Entity.DATA_ID, 1402 Contacts.Entity.CONTACT_ID, 1403 Contacts.Entity.IS_SUPER_PRIMARY, 1404 }; 1405 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" + 1406 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME + 1407 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " + 1408 " AND " + StructuredName.DISPLAY_NAME + " != '' "; 1409 1410 int DATA_ID = 0; 1411 int CONTACT_ID = 1; 1412 int IS_SUPER_PRIMARY = 2; 1413 } 1414 joinSeveralContacts(Intent intent)1415 private void joinSeveralContacts(Intent intent) { 1416 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS); 1417 1418 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); 1419 1420 // Load raw contact IDs for all contacts involved. 1421 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds); 1422 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds); 1423 if (rawContactIds == null) { 1424 Log.e(TAG, "Invalid arguments for joinSeveralContacts request"); 1425 if (receiver != null) { 1426 receiver.send(BAD_ARGUMENTS, new Bundle()); 1427 } 1428 return; 1429 } 1430 1431 // For each pair of raw contacts, insert an aggregation exception 1432 final ContentResolver resolver = getContentResolver(); 1433 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225 1434 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE; 1435 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize); 1436 for (int i = 0; i < rawContactIds.length; i++) { 1437 for (int j = 0; j < rawContactIds.length; j++) { 1438 if (i != j) { 1439 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 1440 } 1441 // Before we get to 500 we need to flush the operations list 1442 if (operations.size() > 0 && operations.size() % batchSize == 0) { 1443 if (!applyOperations(resolver, operations)) { 1444 if (receiver != null) { 1445 receiver.send(CP2_ERROR, new Bundle()); 1446 } 1447 return; 1448 } 1449 operations.clear(); 1450 } 1451 } 1452 } 1453 if (operations.size() > 0 && !applyOperations(resolver, operations)) { 1454 if (receiver != null) { 1455 receiver.send(CP2_ERROR, new Bundle()); 1456 } 1457 return; 1458 } 1459 1460 1461 final String name = queryNameOfLinkedContacts(contactIds); 1462 if (name != null) { 1463 if (receiver != null) { 1464 final Bundle result = new Bundle(); 1465 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds); 1466 result.putString(EXTRA_DISPLAY_NAME, name); 1467 receiver.send(CONTACTS_LINKED, result); 1468 } else { 1469 if (TextUtils.isEmpty(name)) { 1470 showToast(R.string.contactsJoinedMessage); 1471 } else { 1472 showToast(R.string.contactsJoinedNamedMessage, name); 1473 } 1474 } 1475 LocalBroadcastManager.getInstance(this) 1476 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE)); 1477 } else { 1478 if (receiver != null) { 1479 receiver.send(CP2_ERROR, new Bundle()); 1480 } 1481 showToast(R.string.contactJoinErrorToast); 1482 } 1483 } 1484 1485 /** Get the display name of the top-level contact after the contacts have been linked. */ queryNameOfLinkedContacts(long[] contactIds)1486 private String queryNameOfLinkedContacts(long[] contactIds) { 1487 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN ("); 1488 final String[] whereArgs = new String[contactIds.length]; 1489 for (int i = 0; i < contactIds.length; i++) { 1490 whereArgs[i] = String.valueOf(contactIds[i]); 1491 whereBuilder.append("?,"); 1492 } 1493 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')'); 1494 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI, 1495 new String[]{Contacts._ID, Contacts.DISPLAY_NAME, 1496 Contacts.DISPLAY_NAME_ALTERNATIVE}, 1497 whereBuilder.toString(), whereArgs, null); 1498 1499 String name = null; 1500 String nameAlt = null; 1501 long contactId = 0; 1502 try { 1503 if (cursor.moveToFirst()) { 1504 contactId = cursor.getLong(0); 1505 name = cursor.getString(1); 1506 nameAlt = cursor.getString(2); 1507 } 1508 while(cursor.moveToNext()) { 1509 if (cursor.getLong(0) != contactId) { 1510 return null; 1511 } 1512 } 1513 1514 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt, 1515 new ContactsPreferences(getApplicationContext())); 1516 return formattedName == null ? "" : formattedName; 1517 } finally { 1518 if (cursor != null) { 1519 cursor.close(); 1520 } 1521 } 1522 } 1523 1524 /** Returns true if the batch was successfully applied and false otherwise. */ applyOperations(ContentResolver resolver, ArrayList<ContentProviderOperation> operations)1525 private boolean applyOperations(ContentResolver resolver, 1526 ArrayList<ContentProviderOperation> operations) { 1527 try { 1528 final ContentProviderResult[] result = 1529 resolver.applyBatch(ContactsContract.AUTHORITY, operations); 1530 for (int i = 0; i < result.length; ++i) { 1531 // if no rows were modified in the operation then we count it as fail. 1532 if (result[i].count < 0) { 1533 throw new OperationApplicationException(); 1534 } 1535 } 1536 return true; 1537 } catch (RemoteException | OperationApplicationException e) { 1538 FeedbackHelper.sendFeedback(this, TAG, 1539 "Failed to apply aggregation exception batch", e); 1540 showToast(R.string.contactSavedErrorToast); 1541 return false; 1542 } 1543 } 1544 joinContacts(Intent intent)1545 private void joinContacts(Intent intent) { 1546 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1); 1547 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1); 1548 1549 // Load raw contact IDs for all raw contacts involved - currently edited and selected 1550 // in the join UIs. 1551 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2); 1552 if (rawContactIds == null) { 1553 Log.e(TAG, "Invalid arguments for joinContacts request"); 1554 return; 1555 } 1556 1557 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 1558 1559 // For each pair of raw contacts, insert an aggregation exception 1560 for (int i = 0; i < rawContactIds.length; i++) { 1561 for (int j = 0; j < rawContactIds.length; j++) { 1562 if (i != j) { 1563 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 1564 } 1565 } 1566 } 1567 1568 final ContentResolver resolver = getContentResolver(); 1569 1570 // Use the name for contactId1 as the name for the newly aggregated contact. 1571 final Uri contactId1Uri = ContentUris.withAppendedId( 1572 Contacts.CONTENT_URI, contactId1); 1573 final Uri entityUri = Uri.withAppendedPath( 1574 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY); 1575 Cursor c = resolver.query(entityUri, 1576 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null); 1577 if (c == null) { 1578 Log.e(TAG, "Unable to open Contacts DB cursor"); 1579 showToast(R.string.contactSavedErrorToast); 1580 return; 1581 } 1582 long dataIdToAddSuperPrimary = -1; 1583 try { 1584 if (c.moveToFirst()) { 1585 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID); 1586 } 1587 } finally { 1588 c.close(); 1589 } 1590 1591 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact 1592 // display name does not change as a result of the join. 1593 if (dataIdToAddSuperPrimary != -1) { 1594 Builder builder = ContentProviderOperation.newUpdate( 1595 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary)); 1596 builder.withValue(Data.IS_SUPER_PRIMARY, 1); 1597 builder.withValue(Data.IS_PRIMARY, 1); 1598 operations.add(builder.build()); 1599 } 1600 1601 // Apply all aggregation exceptions as one batch 1602 final boolean success = applyOperations(resolver, operations); 1603 1604 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2}); 1605 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT); 1606 if (success && name != null) { 1607 if (TextUtils.isEmpty(name)) { 1608 showToast(R.string.contactsJoinedMessage); 1609 } else { 1610 showToast(R.string.contactsJoinedNamedMessage, name); 1611 } 1612 Uri uri = RawContacts.getContactLookupUri(resolver, 1613 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0])); 1614 callbackIntent.setData(uri); 1615 LocalBroadcastManager.getInstance(this) 1616 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE)); 1617 } 1618 deliverCallback(callbackIntent); 1619 } 1620 1621 /** 1622 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer 1623 * array of the return value holds an array of raw contact ids for one contactId. 1624 * @param contactIds 1625 * @return 1626 */ getSeparatedRawContactIds(long[] contactIds)1627 private long[][] getSeparatedRawContactIds(long[] contactIds) { 1628 final long[][] rawContactIds = new long[contactIds.length][]; 1629 for (int i = 0; i < contactIds.length; i++) { 1630 rawContactIds[i] = getRawContactIds(contactIds[i]); 1631 } 1632 return rawContactIds; 1633 } 1634 1635 /** 1636 * Gets the raw contact ids associated with {@param contactId}. 1637 * @param contactId 1638 * @return Array of raw contact ids. 1639 */ getRawContactIds(long contactId)1640 private long[] getRawContactIds(long contactId) { 1641 final ContentResolver resolver = getContentResolver(); 1642 long rawContactIds[]; 1643 1644 final StringBuilder queryBuilder = new StringBuilder(); 1645 queryBuilder.append(RawContacts.CONTACT_ID) 1646 .append("=") 1647 .append(String.valueOf(contactId)); 1648 1649 final Cursor c = resolver.query(RawContacts.CONTENT_URI, 1650 JoinContactQuery.PROJECTION, 1651 queryBuilder.toString(), 1652 null, null); 1653 if (c == null) { 1654 Log.e(TAG, "Unable to open Contacts DB cursor"); 1655 return null; 1656 } 1657 try { 1658 rawContactIds = new long[c.getCount()]; 1659 for (int i = 0; i < rawContactIds.length; i++) { 1660 c.moveToPosition(i); 1661 final long rawContactId = c.getLong(JoinContactQuery._ID); 1662 rawContactIds[i] = rawContactId; 1663 } 1664 } finally { 1665 c.close(); 1666 } 1667 return rawContactIds; 1668 } 1669 getRawContactIdsForAggregation(long[] contactIds)1670 private long[] getRawContactIdsForAggregation(long[] contactIds) { 1671 if (contactIds == null) { 1672 return null; 1673 } 1674 1675 final ContentResolver resolver = getContentResolver(); 1676 1677 final StringBuilder queryBuilder = new StringBuilder(); 1678 final String stringContactIds[] = new String[contactIds.length]; 1679 for (int i = 0; i < contactIds.length; i++) { 1680 queryBuilder.append(RawContacts.CONTACT_ID + "=?"); 1681 stringContactIds[i] = String.valueOf(contactIds[i]); 1682 if (contactIds[i] == -1) { 1683 return null; 1684 } 1685 if (i == contactIds.length -1) { 1686 break; 1687 } 1688 queryBuilder.append(" OR "); 1689 } 1690 1691 final Cursor c = resolver.query(RawContacts.CONTENT_URI, 1692 JoinContactQuery.PROJECTION, 1693 queryBuilder.toString(), 1694 stringContactIds, null); 1695 if (c == null) { 1696 Log.e(TAG, "Unable to open Contacts DB cursor"); 1697 showToast(R.string.contactSavedErrorToast); 1698 return null; 1699 } 1700 long rawContactIds[]; 1701 try { 1702 if (c.getCount() < 2) { 1703 Log.e(TAG, "Not enough raw contacts to aggregate together."); 1704 return null; 1705 } 1706 rawContactIds = new long[c.getCount()]; 1707 for (int i = 0; i < rawContactIds.length; i++) { 1708 c.moveToPosition(i); 1709 long rawContactId = c.getLong(JoinContactQuery._ID); 1710 rawContactIds[i] = rawContactId; 1711 } 1712 } finally { 1713 c.close(); 1714 } 1715 return rawContactIds; 1716 } 1717 getRawContactIdsForAggregation(long contactId1, long contactId2)1718 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) { 1719 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2}); 1720 } 1721 1722 /** 1723 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation. 1724 */ buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1, long rawContactId2)1725 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, 1726 long rawContactId1, long rawContactId2) { 1727 Builder builder = 1728 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 1729 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); 1730 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 1731 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 1732 operations.add(builder.build()); 1733 } 1734 1735 /** 1736 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a 1737 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is 1738 * requested. 1739 */ buildSplitContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1, long rawContactId2, boolean hardSplit)1740 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations, 1741 long rawContactId1, long rawContactId2, boolean hardSplit) { 1742 final Builder builder = 1743 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 1744 builder.withValue(AggregationExceptions.TYPE, 1745 hardSplit 1746 ? AggregationExceptions.TYPE_KEEP_SEPARATE 1747 : AggregationExceptions.TYPE_AUTOMATIC); 1748 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 1749 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 1750 operations.add(builder.build()); 1751 } 1752 1753 /** 1754 * Returns an intent that can start this service and cause it to sleep for the specified time. 1755 * 1756 * This exists purely for debugging and manual testing. Since this service uses a single thread 1757 * it is useful to have a way to test behavior when work is queued up and most of the other 1758 * operations complete too quickly to simulate that under normal conditions. 1759 */ createSleepIntent(Context context, long millis)1760 public static Intent createSleepIntent(Context context, long millis) { 1761 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP) 1762 .putExtra(EXTRA_SLEEP_DURATION, millis); 1763 } 1764 sleepForDebugging(Intent intent)1765 private void sleepForDebugging(Intent intent) { 1766 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000); 1767 if (Log.isLoggable(TAG, Log.DEBUG)) { 1768 Log.d(TAG, "sleeping for " + duration + "ms"); 1769 } 1770 try { 1771 Thread.sleep(duration); 1772 } catch (InterruptedException e) { 1773 e.printStackTrace(); 1774 } 1775 if (Log.isLoggable(TAG, Log.DEBUG)) { 1776 Log.d(TAG, "finished sleeping"); 1777 } 1778 } 1779 1780 /** 1781 * Shows a toast on the UI thread by formatting messageId using args. 1782 * @param messageId id of message string 1783 * @param args args to format string 1784 */ showToast(final int messageId, final Object... args)1785 private void showToast(final int messageId, final Object... args) { 1786 final String message = getResources().getString(messageId, args); 1787 mMainHandler.post(new Runnable() { 1788 @Override 1789 public void run() { 1790 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show(); 1791 } 1792 }); 1793 } 1794 1795 1796 /** 1797 * Shows a toast on the UI thread. 1798 */ showToast(final int message)1799 private void showToast(final int message) { 1800 mMainHandler.post(new Runnable() { 1801 1802 @Override 1803 public void run() { 1804 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show(); 1805 } 1806 }); 1807 } 1808 deliverCallback(final Intent callbackIntent)1809 private void deliverCallback(final Intent callbackIntent) { 1810 mMainHandler.post(new Runnable() { 1811 1812 @Override 1813 public void run() { 1814 deliverCallbackOnUiThread(callbackIntent); 1815 } 1816 }); 1817 } 1818 deliverCallbackOnUiThread(final Intent callbackIntent)1819 void deliverCallbackOnUiThread(final Intent callbackIntent) { 1820 // TODO: this assumes that if there are multiple instances of the same 1821 // activity registered, the last one registered is the one waiting for 1822 // the callback. Validity of this assumption needs to be verified. 1823 for (Listener listener : sListeners) { 1824 if (callbackIntent.getComponent().equals( 1825 ((Activity) listener).getIntent().getComponent())) { 1826 listener.onServiceCompleted(callbackIntent); 1827 return; 1828 } 1829 } 1830 } 1831 1832 public interface GroupsDao { create(String title, AccountWithDataSet account)1833 Uri create(String title, AccountWithDataSet account); delete(Uri groupUri)1834 int delete(Uri groupUri); captureDeletionUndoData(Uri groupUri)1835 Bundle captureDeletionUndoData(Uri groupUri); undoDeletion(Bundle undoData)1836 Uri undoDeletion(Bundle undoData); 1837 } 1838 1839 public static class GroupsDaoImpl implements GroupsDao { 1840 public static final String KEY_GROUP_DATA = "groupData"; 1841 public static final String KEY_GROUP_MEMBERS = "groupMemberIds"; 1842 1843 private static final String TAG = "GroupsDao"; 1844 private final Context context; 1845 private final ContentResolver contentResolver; 1846 GroupsDaoImpl(Context context)1847 public GroupsDaoImpl(Context context) { 1848 this(context, context.getContentResolver()); 1849 } 1850 GroupsDaoImpl(Context context, ContentResolver contentResolver)1851 public GroupsDaoImpl(Context context, ContentResolver contentResolver) { 1852 this.context = context; 1853 this.contentResolver = contentResolver; 1854 } 1855 captureDeletionUndoData(Uri groupUri)1856 public Bundle captureDeletionUndoData(Uri groupUri) { 1857 final long groupId = ContentUris.parseId(groupUri); 1858 final Bundle result = new Bundle(); 1859 1860 final Cursor cursor = contentResolver.query(groupUri, 1861 new String[]{ 1862 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE, 1863 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET, 1864 Groups.SHOULD_SYNC 1865 }, 1866 Groups.DELETED + "=?", new String[] { "0" }, null); 1867 try { 1868 if (cursor.moveToFirst()) { 1869 final ContentValues groupValues = new ContentValues(); 1870 DatabaseUtils.cursorRowToContentValues(cursor, groupValues); 1871 result.putParcelable(KEY_GROUP_DATA, groupValues); 1872 } else { 1873 // Group doesn't exist. 1874 return result; 1875 } 1876 } finally { 1877 cursor.close(); 1878 } 1879 1880 final Cursor membersCursor = contentResolver.query( 1881 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID }, 1882 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?", 1883 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null); 1884 final long[] memberIds = new long[membersCursor.getCount()]; 1885 int i = 0; 1886 while (membersCursor.moveToNext()) { 1887 memberIds[i++] = membersCursor.getLong(0); 1888 } 1889 result.putLongArray(KEY_GROUP_MEMBERS, memberIds); 1890 return result; 1891 } 1892 undoDeletion(Bundle deletedGroupData)1893 public Uri undoDeletion(Bundle deletedGroupData) { 1894 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA); 1895 if (groupData == null) { 1896 return null; 1897 } 1898 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData); 1899 final long groupId = ContentUris.parseId(groupUri); 1900 1901 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS); 1902 if (memberIds == null) { 1903 return groupUri; 1904 } 1905 final ContentValues[] memberInsertions = new ContentValues[memberIds.length]; 1906 for (int i = 0; i < memberIds.length; i++) { 1907 memberInsertions[i] = new ContentValues(); 1908 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]); 1909 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 1910 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId); 1911 } 1912 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions); 1913 if (inserted != memberIds.length) { 1914 Log.e(TAG, "Could not recover some members for group deletion undo"); 1915 } 1916 1917 return groupUri; 1918 } 1919 create(String title, AccountWithDataSet account)1920 public Uri create(String title, AccountWithDataSet account) { 1921 final ContentValues values = new ContentValues(); 1922 values.put(Groups.TITLE, title); 1923 values.put(Groups.ACCOUNT_NAME, account.name); 1924 values.put(Groups.ACCOUNT_TYPE, account.type); 1925 values.put(Groups.DATA_SET, account.dataSet); 1926 return contentResolver.insert(Groups.CONTENT_URI, values); 1927 } 1928 delete(Uri groupUri)1929 public int delete(Uri groupUri) { 1930 return contentResolver.delete(groupUri, null, null); 1931 } 1932 } 1933 1934 /** 1935 * Keeps track of which operations have been requested but have not yet finished for this 1936 * service. 1937 */ 1938 public static class State { 1939 private final CopyOnWriteArrayList<Intent> mPending; 1940 State()1941 public State() { 1942 mPending = new CopyOnWriteArrayList<>(); 1943 } 1944 State(Collection<Intent> pendingActions)1945 public State(Collection<Intent> pendingActions) { 1946 mPending = new CopyOnWriteArrayList<>(pendingActions); 1947 } 1948 isIdle()1949 public boolean isIdle() { 1950 return mPending.isEmpty(); 1951 } 1952 getCurrentIntent()1953 public Intent getCurrentIntent() { 1954 return mPending.isEmpty() ? null : mPending.get(0); 1955 } 1956 1957 /** 1958 * Returns the first intent requested that has the specified action or null if no intent 1959 * with that action has been requested. 1960 */ getNextIntentWithAction(String action)1961 public Intent getNextIntentWithAction(String action) { 1962 for (Intent intent : mPending) { 1963 if (action.equals(intent.getAction())) { 1964 return intent; 1965 } 1966 } 1967 return null; 1968 } 1969 isActionPending(String action)1970 public boolean isActionPending(String action) { 1971 return getNextIntentWithAction(action) != null; 1972 } 1973 onFinish(Intent intent)1974 private void onFinish(Intent intent) { 1975 if (mPending.isEmpty()) { 1976 return; 1977 } 1978 final String action = mPending.get(0).getAction(); 1979 if (action.equals(intent.getAction())) { 1980 mPending.remove(0); 1981 } 1982 } 1983 onStart(Intent intent)1984 private void onStart(Intent intent) { 1985 if (intent.getAction() == null) { 1986 return; 1987 } 1988 mPending.add(intent); 1989 } 1990 } 1991 } 1992