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