1 /* 2 * Copyright (C) 2009 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.ui; 18 19 import com.android.contacts.ContactsListActivity; 20 import com.android.contacts.ContactsUtils; 21 import com.android.contacts.R; 22 import com.android.contacts.model.ContactsSource; 23 import com.android.contacts.model.Editor; 24 import com.android.contacts.model.EntityDelta; 25 import com.android.contacts.model.EntityModifier; 26 import com.android.contacts.model.EntitySet; 27 import com.android.contacts.model.GoogleSource; 28 import com.android.contacts.model.Sources; 29 import com.android.contacts.model.ContactsSource.EditType; 30 import com.android.contacts.model.Editor.EditorListener; 31 import com.android.contacts.model.EntityDelta.ValuesDelta; 32 import com.android.contacts.ui.widget.BaseContactEditorView; 33 import com.android.contacts.ui.widget.PhotoEditorView; 34 import com.android.contacts.util.EmptyService; 35 import com.android.contacts.util.WeakAsyncTask; 36 import com.google.android.collect.Lists; 37 38 import android.accounts.Account; 39 import android.app.Activity; 40 import android.app.AlertDialog; 41 import android.app.Dialog; 42 import android.app.ProgressDialog; 43 import android.content.ActivityNotFoundException; 44 import android.content.ContentProviderOperation; 45 import android.content.ContentProviderResult; 46 import android.content.ContentResolver; 47 import android.content.ContentUris; 48 import android.content.ContentValues; 49 import android.content.Context; 50 import android.content.DialogInterface; 51 import android.content.Entity; 52 import android.content.Intent; 53 import android.content.OperationApplicationException; 54 import android.content.ContentProviderOperation.Builder; 55 import android.database.Cursor; 56 import android.graphics.Bitmap; 57 import android.net.Uri; 58 import android.os.Bundle; 59 import android.os.RemoteException; 60 import android.provider.ContactsContract; 61 import android.provider.ContactsContract.AggregationExceptions; 62 import android.provider.ContactsContract.Contacts; 63 import android.provider.ContactsContract.RawContacts; 64 import android.provider.ContactsContract.CommonDataKinds.Email; 65 import android.provider.ContactsContract.CommonDataKinds.Phone; 66 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 67 import android.provider.ContactsContract.Contacts.Data; 68 import android.util.Log; 69 import android.view.ContextThemeWrapper; 70 import android.view.LayoutInflater; 71 import android.view.Menu; 72 import android.view.MenuInflater; 73 import android.view.MenuItem; 74 import android.view.View; 75 import android.view.ViewGroup; 76 import android.widget.ArrayAdapter; 77 import android.widget.LinearLayout; 78 import android.widget.ListAdapter; 79 import android.widget.TextView; 80 import android.widget.Toast; 81 82 import java.lang.ref.WeakReference; 83 import java.util.ArrayList; 84 import java.util.Collections; 85 import java.util.Comparator; 86 87 /** 88 * Activity for editing or inserting a contact. 89 */ 90 public final class EditContactActivity extends Activity 91 implements View.OnClickListener, Comparator<EntityDelta> { 92 private static final String TAG = "EditContactActivity"; 93 94 /** The launch code when picking a photo and the raw data is returned */ 95 private static final int PHOTO_PICKED_WITH_DATA = 3021; 96 97 /** The launch code when a contact to join with is returned */ 98 private static final int REQUEST_JOIN_CONTACT = 3022; 99 100 private static final String KEY_EDIT_STATE = "state"; 101 private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; 102 103 /** The result code when view activity should close after edit returns */ 104 public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777; 105 106 public static final int SAVE_MODE_DEFAULT = 0; 107 public static final int SAVE_MODE_SPLIT = 1; 108 public static final int SAVE_MODE_JOIN = 2; 109 110 private long mRawContactIdRequestingPhoto = -1; 111 112 private static final int DIALOG_CONFIRM_DELETE = 1; 113 private static final int DIALOG_CONFIRM_READONLY_DELETE = 2; 114 private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3; 115 private static final int DIALOG_CONFIRM_READONLY_HIDE = 4; 116 117 String mQuerySelection; 118 119 private long mContactIdForJoin; 120 EntitySet mState; 121 122 /** The linear layout holding the ContactEditorViews */ 123 LinearLayout mContent; 124 125 private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList(); 126 127 @Override onCreate(Bundle icicle)128 protected void onCreate(Bundle icicle) { 129 super.onCreate(icicle); 130 131 final Intent intent = getIntent(); 132 final String action = intent.getAction(); 133 134 setContentView(R.layout.act_edit); 135 136 // Build editor and listen for photo requests 137 mContent = (LinearLayout) findViewById(R.id.editors); 138 139 findViewById(R.id.btn_done).setOnClickListener(this); 140 findViewById(R.id.btn_discard).setOnClickListener(this); 141 142 // Handle initial actions only when existing state missing 143 final boolean hasIncomingState = icicle != null && icicle.containsKey(KEY_EDIT_STATE); 144 145 if (Intent.ACTION_EDIT.equals(action) && !hasIncomingState) { 146 // Read initial state from database 147 new QueryEntitiesTask(this).execute(intent); 148 } else if (Intent.ACTION_INSERT.equals(action) && !hasIncomingState) { 149 // Trigger dialog to pick account type 150 doAddAction(); 151 } 152 } 153 154 private static class QueryEntitiesTask extends 155 WeakAsyncTask<Intent, Void, Void, EditContactActivity> { QueryEntitiesTask(EditContactActivity target)156 public QueryEntitiesTask(EditContactActivity target) { 157 super(target); 158 } 159 160 @Override doInBackground(EditContactActivity target, Intent... params)161 protected Void doInBackground(EditContactActivity target, Intent... params) { 162 // Load edit details in background 163 final Context context = target; 164 final Sources sources = Sources.getInstance(context); 165 final Intent intent = params[0]; 166 167 final ContentResolver resolver = context.getContentResolver(); 168 169 // Handle both legacy and new authorities 170 final Uri data = intent.getData(); 171 final String authority = data.getAuthority(); 172 final String mimeType = intent.resolveType(resolver); 173 174 String selection = "0"; 175 if (ContactsContract.AUTHORITY.equals(authority)) { 176 if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) { 177 // Handle selected aggregate 178 final long contactId = ContentUris.parseId(data); 179 selection = RawContacts.CONTACT_ID + "=" + contactId; 180 } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) { 181 final long rawContactId = ContentUris.parseId(data); 182 final long contactId = ContactsUtils.queryForContactId(resolver, rawContactId); 183 selection = RawContacts.CONTACT_ID + "=" + contactId; 184 } 185 } else if (android.provider.Contacts.AUTHORITY.equals(authority)) { 186 final long rawContactId = ContentUris.parseId(data); 187 selection = Data.RAW_CONTACT_ID + "=" + rawContactId; 188 } 189 190 target.mQuerySelection = selection; 191 target.mState = EntitySet.fromQuery(resolver, selection, null, null); 192 193 // Handle any incoming values that should be inserted 194 final Bundle extras = intent.getExtras(); 195 final boolean hasExtras = extras != null && extras.size() > 0; 196 final boolean hasState = target.mState.size() > 0; 197 if (hasExtras && hasState) { 198 // Find source defining the first RawContact found 199 final EntityDelta state = target.mState.get(0); 200 final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 201 final ContactsSource source = sources.getInflatedSource(accountType, 202 ContactsSource.LEVEL_CONSTRAINTS); 203 EntityModifier.parseExtras(context, source, state, extras); 204 } 205 206 return null; 207 } 208 209 @Override onPostExecute(EditContactActivity target, Void result)210 protected void onPostExecute(EditContactActivity target, Void result) { 211 // Bind UI to new background state 212 target.bindEditors(); 213 } 214 } 215 216 217 218 @Override onSaveInstanceState(Bundle outState)219 protected void onSaveInstanceState(Bundle outState) { 220 if (hasValidState()) { 221 // Store entities with modifications 222 outState.putParcelable(KEY_EDIT_STATE, mState); 223 } 224 225 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 226 super.onSaveInstanceState(outState); 227 } 228 229 @Override onRestoreInstanceState(Bundle savedInstanceState)230 protected void onRestoreInstanceState(Bundle savedInstanceState) { 231 // Read modifications from instance 232 mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE); 233 mRawContactIdRequestingPhoto = savedInstanceState.getLong( 234 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); 235 bindEditors(); 236 237 super.onRestoreInstanceState(savedInstanceState); 238 } 239 240 @Override onDestroy()241 protected void onDestroy() { 242 super.onDestroy(); 243 244 for (Dialog dialog : mManagedDialogs) { 245 if (dialog.isShowing()) { 246 dialog.dismiss(); 247 } 248 } 249 } 250 251 @Override onCreateDialog(int id)252 protected Dialog onCreateDialog(int id) { 253 switch (id) { 254 case DIALOG_CONFIRM_DELETE: 255 return new AlertDialog.Builder(this) 256 .setTitle(R.string.deleteConfirmation_title) 257 .setIcon(android.R.drawable.ic_dialog_alert) 258 .setMessage(R.string.deleteConfirmation) 259 .setNegativeButton(android.R.string.cancel, null) 260 .setPositiveButton(android.R.string.ok, new DeleteClickListener()) 261 .setCancelable(false) 262 .create(); 263 case DIALOG_CONFIRM_READONLY_DELETE: 264 return new AlertDialog.Builder(this) 265 .setTitle(R.string.deleteConfirmation_title) 266 .setIcon(android.R.drawable.ic_dialog_alert) 267 .setMessage(R.string.readOnlyContactDeleteConfirmation) 268 .setNegativeButton(android.R.string.cancel, null) 269 .setPositiveButton(android.R.string.ok, new DeleteClickListener()) 270 .setCancelable(false) 271 .create(); 272 case DIALOG_CONFIRM_MULTIPLE_DELETE: 273 return new AlertDialog.Builder(this) 274 .setTitle(R.string.deleteConfirmation_title) 275 .setIcon(android.R.drawable.ic_dialog_alert) 276 .setMessage(R.string.multipleContactDeleteConfirmation) 277 .setNegativeButton(android.R.string.cancel, null) 278 .setPositiveButton(android.R.string.ok, new DeleteClickListener()) 279 .setCancelable(false) 280 .create(); 281 case DIALOG_CONFIRM_READONLY_HIDE: 282 return new AlertDialog.Builder(this) 283 .setTitle(R.string.deleteConfirmation_title) 284 .setIcon(android.R.drawable.ic_dialog_alert) 285 .setMessage(R.string.readOnlyContactWarning) 286 .setPositiveButton(android.R.string.ok, new DeleteClickListener()) 287 .setCancelable(false) 288 .create(); 289 } 290 return null; 291 } 292 293 /** 294 * Start managing this {@link Dialog} along with the {@link Activity}. 295 */ startManagingDialog(Dialog dialog)296 private void startManagingDialog(Dialog dialog) { 297 synchronized (mManagedDialogs) { 298 mManagedDialogs.add(dialog); 299 } 300 } 301 302 /** 303 * Show this {@link Dialog} and manage with the {@link Activity}. 304 */ showAndManageDialog(Dialog dialog)305 void showAndManageDialog(Dialog dialog) { 306 startManagingDialog(dialog); 307 dialog.show(); 308 } 309 310 /** 311 * Check if our internal {@link #mState} is valid, usually checked before 312 * performing user actions. 313 */ hasValidState()314 protected boolean hasValidState() { 315 return mState != null && mState.size() > 0; 316 } 317 318 /** 319 * Rebuild the editors to match our underlying {@link #mState} object, usually 320 * called once we've parsed {@link Entity} data or have inserted a new 321 * {@link RawContacts}. 322 */ bindEditors()323 protected void bindEditors() { 324 if (!hasValidState()) return; 325 326 final LayoutInflater inflater = (LayoutInflater) getSystemService( 327 Context.LAYOUT_INFLATER_SERVICE); 328 final Sources sources = Sources.getInstance(this); 329 330 // Sort the editors 331 Collections.sort(mState, this); 332 333 // Remove any existing editors and rebuild any visible 334 mContent.removeAllViews(); 335 int size = mState.size(); 336 for (int i = 0; i < size; i++) { 337 // TODO ensure proper ordering of entities in the list 338 EntityDelta entity = mState.get(i); 339 final ValuesDelta values = entity.getValues(); 340 if (!values.isVisible()) continue; 341 342 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 343 final ContactsSource source = sources.getInflatedSource(accountType, 344 ContactsSource.LEVEL_CONSTRAINTS); 345 final long rawContactId = values.getAsLong(RawContacts._ID); 346 347 BaseContactEditorView editor; 348 if (!source.readOnly) { 349 editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor, 350 mContent, false); 351 } else { 352 editor = (BaseContactEditorView) inflater.inflate( 353 R.layout.item_read_only_contact_editor, mContent, false); 354 } 355 PhotoEditorView photoEditor = editor.getPhotoEditor(); 356 photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly, 357 photoEditor)); 358 359 mContent.addView(editor); 360 editor.setState(entity, source); 361 } 362 363 // Show editor now that we've loaded state 364 mContent.setVisibility(View.VISIBLE); 365 } 366 367 /** 368 * Class that listens to requests coming from photo editors 369 */ 370 private class PhotoListener implements EditorListener, DialogInterface.OnClickListener { 371 private long mRawContactId; 372 private boolean mReadOnly; 373 private PhotoEditorView mEditor; 374 PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor)375 public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) { 376 mRawContactId = rawContactId; 377 mReadOnly = readOnly; 378 mEditor = editor; 379 } 380 onDeleted(Editor editor)381 public void onDeleted(Editor editor) { 382 // Do nothing 383 } 384 onRequest(int request)385 public void onRequest(int request) { 386 if (!hasValidState()) return; 387 388 if (request == EditorListener.REQUEST_PICK_PHOTO) { 389 if (mEditor.hasSetPhoto()) { 390 // There is an existing photo, offer to remove, replace, or promoto to primary 391 createPhotoDialog().show(); 392 } else if (!mReadOnly) { 393 // No photo set and not read-only, try to set the photo 394 doPickPhotoAction(mRawContactId); 395 } 396 } 397 } 398 399 /** 400 * Prepare dialog for picking a new {@link EditType} or entering a 401 * custom label. This dialog is limited to the valid types as determined 402 * by {@link EntityModifier}. 403 */ createPhotoDialog()404 public Dialog createPhotoDialog() { 405 Context context = EditContactActivity.this; 406 407 // Wrap our context to inflate list items using correct theme 408 final Context dialogContext = new ContextThemeWrapper(context, 409 android.R.style.Theme_Light); 410 411 String[] choices; 412 if (mReadOnly) { 413 choices = new String[1]; 414 choices[0] = getString(R.string.use_photo_as_primary); 415 } else { 416 choices = new String[3]; 417 choices[0] = getString(R.string.use_photo_as_primary); 418 choices[1] = getString(R.string.removePicture); 419 choices[2] = getString(R.string.changePicture); 420 } 421 final ListAdapter adapter = new ArrayAdapter<String>(dialogContext, 422 android.R.layout.simple_list_item_1, choices); 423 424 final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext); 425 builder.setTitle(R.string.attachToContact); 426 builder.setSingleChoiceItems(adapter, -1, this); 427 return builder.create(); 428 } 429 430 /** 431 * Called when something in the dialog is clicked 432 */ onClick(DialogInterface dialog, int which)433 public void onClick(DialogInterface dialog, int which) { 434 dialog.dismiss(); 435 436 switch (which) { 437 case 0: 438 // Set the photo as super primary 439 mEditor.setSuperPrimary(true); 440 441 // And set all other photos as not super primary 442 int count = mContent.getChildCount(); 443 for (int i = 0; i < count; i++) { 444 View childView = mContent.getChildAt(i); 445 if (childView instanceof BaseContactEditorView) { 446 BaseContactEditorView editor = (BaseContactEditorView) childView; 447 PhotoEditorView photoEditor = editor.getPhotoEditor(); 448 if (!photoEditor.equals(mEditor)) { 449 photoEditor.setSuperPrimary(false); 450 } 451 } 452 } 453 break; 454 455 case 1: 456 // Remove the photo 457 mEditor.setPhotoBitmap(null); 458 break; 459 460 case 2: 461 // Pick a new photo for the contact 462 doPickPhotoAction(mRawContactId); 463 break; 464 } 465 } 466 } 467 468 /** {@inheritDoc} */ onClick(View view)469 public void onClick(View view) { 470 switch (view.getId()) { 471 case R.id.btn_done: 472 doSaveAction(SAVE_MODE_DEFAULT); 473 break; 474 case R.id.btn_discard: 475 doRevertAction(); 476 break; 477 } 478 } 479 480 /** {@inheritDoc} */ 481 @Override onBackPressed()482 public void onBackPressed() { 483 doSaveAction(SAVE_MODE_DEFAULT); 484 } 485 486 /** {@inheritDoc} */ 487 @Override onActivityResult(int requestCode, int resultCode, Intent data)488 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 489 // Ignore failed requests 490 if (resultCode != RESULT_OK) return; 491 492 switch (requestCode) { 493 case PHOTO_PICKED_WITH_DATA: { 494 BaseContactEditorView requestingEditor = null; 495 for (int i = 0; i < mContent.getChildCount(); i++) { 496 View childView = mContent.getChildAt(i); 497 if (childView instanceof BaseContactEditorView) { 498 BaseContactEditorView editor = (BaseContactEditorView) childView; 499 if (editor.getRawContactId() == mRawContactIdRequestingPhoto) { 500 requestingEditor = editor; 501 break; 502 } 503 } 504 } 505 506 if (requestingEditor != null) { 507 final Bitmap photo = data.getParcelableExtra("data"); 508 requestingEditor.setPhotoBitmap(photo); 509 mRawContactIdRequestingPhoto = -1; 510 } else { 511 // The contact that requested the photo is no longer present. 512 // TODO: Show error message 513 } 514 515 break; 516 } 517 518 case REQUEST_JOIN_CONTACT: { 519 if (resultCode == RESULT_OK && data != null) { 520 final long contactId = ContentUris.parseId(data.getData()); 521 joinAggregate(contactId); 522 } 523 } 524 } 525 } 526 527 @Override onCreateOptionsMenu(Menu menu)528 public boolean onCreateOptionsMenu(Menu menu) { 529 super.onCreateOptionsMenu(menu); 530 531 MenuInflater inflater = getMenuInflater(); 532 inflater.inflate(R.menu.edit, menu); 533 534 535 return true; 536 } 537 538 @Override onPrepareOptionsMenu(Menu menu)539 public boolean onPrepareOptionsMenu(Menu menu) { 540 menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1); 541 return true; 542 } 543 544 @Override onOptionsItemSelected(MenuItem item)545 public boolean onOptionsItemSelected(MenuItem item) { 546 switch (item.getItemId()) { 547 case R.id.menu_done: 548 return doSaveAction(SAVE_MODE_DEFAULT); 549 case R.id.menu_discard: 550 return doRevertAction(); 551 case R.id.menu_add: 552 return doAddAction(); 553 case R.id.menu_delete: 554 return doDeleteAction(); 555 case R.id.menu_split: 556 return doSplitContactAction(); 557 case R.id.menu_join: 558 return doJoinContactAction(); 559 } 560 return false; 561 } 562 563 /** 564 * Background task for persisting edited contact data, using the changes 565 * defined by a set of {@link EntityDelta}. This task starts 566 * {@link EmptyService} to make sure the background thread can finish 567 * persisting in cases where the system wants to reclaim our process. 568 */ 569 public static class PersistTask extends 570 WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> { 571 private static final int PERSIST_TRIES = 3; 572 573 private static final int RESULT_UNCHANGED = 0; 574 private static final int RESULT_SUCCESS = 1; 575 private static final int RESULT_FAILURE = 2; 576 577 private WeakReference<ProgressDialog> progress; 578 579 private int mSaveMode; 580 private Uri mContactLookupUri = null; 581 PersistTask(EditContactActivity target, int saveMode)582 public PersistTask(EditContactActivity target, int saveMode) { 583 super(target); 584 mSaveMode = saveMode; 585 } 586 587 /** {@inheritDoc} */ 588 @Override onPreExecute(EditContactActivity target)589 protected void onPreExecute(EditContactActivity target) { 590 this.progress = new WeakReference<ProgressDialog>(ProgressDialog.show(target, null, 591 target.getText(R.string.savingContact))); 592 593 // Before starting this task, start an empty service to protect our 594 // process from being reclaimed by the system. 595 final Context context = target; 596 context.startService(new Intent(context, EmptyService.class)); 597 } 598 599 /** {@inheritDoc} */ 600 @Override doInBackground(EditContactActivity target, EntitySet... params)601 protected Integer doInBackground(EditContactActivity target, EntitySet... params) { 602 final Context context = target; 603 final ContentResolver resolver = context.getContentResolver(); 604 605 EntitySet state = params[0]; 606 607 // Trim any empty fields, and RawContacts, before persisting 608 final Sources sources = Sources.getInstance(context); 609 EntityModifier.trimEmpty(state, sources); 610 611 // Attempt to persist changes 612 int tries = 0; 613 Integer result = RESULT_FAILURE; 614 while (tries++ < PERSIST_TRIES) { 615 try { 616 // Build operations and try applying 617 final ArrayList<ContentProviderOperation> diff = state.buildDiff(); 618 ContentProviderResult[] results = null; 619 if (!diff.isEmpty()) { 620 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff); 621 } 622 623 final long rawContactId = getRawContactId(state, diff, results); 624 if (rawContactId != -1) { 625 final Uri rawContactUri = ContentUris.withAppendedId( 626 RawContacts.CONTENT_URI, rawContactId); 627 628 // convert the raw contact URI to a contact URI 629 mContactLookupUri = RawContacts.getContactLookupUri(resolver, 630 rawContactUri); 631 } 632 result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED; 633 break; 634 635 } catch (RemoteException e) { 636 // Something went wrong, bail without success 637 Log.e(TAG, "Problem persisting user edits", e); 638 break; 639 640 } catch (OperationApplicationException e) { 641 // Version consistency failed, re-parent change and try again 642 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString()); 643 final EntitySet newState = EntitySet.fromQuery(resolver, 644 target.mQuerySelection, null, null); 645 state = EntitySet.mergeAfter(newState, state); 646 } 647 } 648 649 return result; 650 } 651 getRawContactId(EntitySet state, final ArrayList<ContentProviderOperation> diff, final ContentProviderResult[] results)652 private long getRawContactId(EntitySet state, 653 final ArrayList<ContentProviderOperation> diff, 654 final ContentProviderResult[] results) { 655 long rawContactId = state.findRawContactId(); 656 if (rawContactId != -1) { 657 return rawContactId; 658 } 659 660 // we gotta do some searching for the id 661 final int diffSize = diff.size(); 662 for (int i = 0; i < diffSize; i++) { 663 ContentProviderOperation operation = diff.get(i); 664 if (operation.getType() == ContentProviderOperation.TYPE_INSERT 665 && operation.getUri().getEncodedPath().contains( 666 RawContacts.CONTENT_URI.getEncodedPath())) { 667 return ContentUris.parseId(results[i].uri); 668 } 669 } 670 return -1; 671 } 672 673 /** {@inheritDoc} */ 674 @Override onPostExecute(EditContactActivity target, Integer result)675 protected void onPostExecute(EditContactActivity target, Integer result) { 676 final Context context = target; 677 678 if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) { 679 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 680 } else if (result == RESULT_FAILURE) { 681 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 682 } 683 684 progress.get().dismiss(); 685 686 // Stop the service that was protecting us 687 context.stopService(new Intent(context, EmptyService.class)); 688 689 target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri); 690 } 691 } 692 693 /** 694 * Saves or creates the contact based on the mode, and if successful 695 * finishes the activity. 696 */ doSaveAction(int saveMode)697 boolean doSaveAction(int saveMode) { 698 if (!hasValidState()) return false; 699 700 final PersistTask task = new PersistTask(this, saveMode); 701 task.execute(mState); 702 703 return true; 704 } 705 706 private class DeleteClickListener implements DialogInterface.OnClickListener { 707 onClick(DialogInterface dialog, int which)708 public void onClick(DialogInterface dialog, int which) { 709 Sources sources = Sources.getInstance(EditContactActivity.this); 710 // Mark all raw contacts for deletion 711 for (EntityDelta delta : mState) { 712 delta.markDeleted(); 713 } 714 // Save the deletes 715 doSaveAction(SAVE_MODE_DEFAULT); 716 finish(); 717 } 718 } 719 onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri)720 private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) { 721 switch (saveMode) { 722 case SAVE_MODE_DEFAULT: 723 if (success && contactLookupUri != null) { 724 final Intent resultIntent = new Intent(); 725 726 final Uri requestData = getIntent().getData(); 727 final String requestAuthority = requestData == null ? null : requestData 728 .getAuthority(); 729 730 if (android.provider.Contacts.AUTHORITY.equals(requestAuthority)) { 731 // Build legacy Uri when requested by caller 732 final long contactId = ContentUris.parseId(Contacts.lookupContact( 733 getContentResolver(), contactLookupUri)); 734 final Uri legacyUri = ContentUris.withAppendedId( 735 android.provider.Contacts.People.CONTENT_URI, contactId); 736 resultIntent.setData(legacyUri); 737 } else { 738 // Otherwise pass back a lookup-style Uri 739 resultIntent.setData(contactLookupUri); 740 } 741 742 setResult(RESULT_OK, resultIntent); 743 } else { 744 setResult(RESULT_CANCELED, null); 745 } 746 finish(); 747 break; 748 749 case SAVE_MODE_SPLIT: 750 if (success) { 751 Intent intent = new Intent(); 752 intent.setData(contactLookupUri); 753 setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent); 754 } 755 finish(); 756 break; 757 758 case SAVE_MODE_JOIN: 759 if (success) { 760 showJoinAggregateActivity(contactLookupUri); 761 } 762 break; 763 } 764 } 765 766 /** 767 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 768 * 769 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 770 */ showJoinAggregateActivity(Uri contactLookupUri)771 public void showJoinAggregateActivity(Uri contactLookupUri) { 772 if (contactLookupUri == null) { 773 return; 774 } 775 776 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 777 Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE); 778 intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin); 779 startActivityForResult(intent, REQUEST_JOIN_CONTACT); 780 } 781 782 /** 783 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 784 */ joinAggregate(final long contactId)785 private void joinAggregate(final long contactId) { 786 ContentResolver resolver = getContentResolver(); 787 788 // Load raw contact IDs for all raw contacts involved - currently edited and selected 789 // in the join UIs 790 Cursor c = resolver.query(RawContacts.CONTENT_URI, 791 new String[] {RawContacts._ID}, 792 RawContacts.CONTACT_ID + "=" + contactId 793 + " OR " + RawContacts.CONTACT_ID + "=" + mContactIdForJoin, null, null); 794 795 long rawContactIds[]; 796 try { 797 rawContactIds = new long[c.getCount()]; 798 for (int i = 0; i < rawContactIds.length; i++) { 799 c.moveToNext(); 800 rawContactIds[i] = c.getLong(0); 801 } 802 } finally { 803 c.close(); 804 } 805 806 // For each pair of raw contacts, insert an aggregation exception 807 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); 808 for (int i = 0; i < rawContactIds.length; i++) { 809 for (int j = 0; j < rawContactIds.length; j++) { 810 if (i != j) { 811 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); 812 } 813 } 814 } 815 816 // Apply all aggregation exceptions as one batch 817 try { 818 getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); 819 820 // We can use any of the constituent raw contacts to refresh the UI - why not the first 821 Intent intent = new Intent(); 822 intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0])); 823 824 // Reload the new state from database 825 new QueryEntitiesTask(this).execute(intent); 826 827 Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show(); 828 } catch (RemoteException e) { 829 Log.e(TAG, "Failed to apply aggregation exception batch", e); 830 Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 831 } catch (OperationApplicationException e) { 832 Log.e(TAG, "Failed to apply aggregation exception batch", e); 833 Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 834 } 835 } 836 837 /** 838 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation. 839 */ buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1, long rawContactId2)840 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, 841 long rawContactId1, long rawContactId2) { 842 Builder builder = 843 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 844 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); 845 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 846 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 847 operations.add(builder.build()); 848 } 849 850 /** 851 * Revert any changes the user has made, and finish the activity. 852 */ doRevertAction()853 private boolean doRevertAction() { 854 finish(); 855 return true; 856 } 857 858 /** 859 * Create a new {@link RawContacts} which will exist as another 860 * {@link EntityDelta} under the currently edited {@link Contacts}. 861 */ doAddAction()862 private boolean doAddAction() { 863 // Adding is okay when missing state 864 new AddContactTask(this).execute(); 865 return true; 866 } 867 868 /** 869 * Delete the entire contact currently being edited, which usually asks for 870 * user confirmation before continuing. 871 */ doDeleteAction()872 private boolean doDeleteAction() { 873 if (!hasValidState()) return false; 874 int readOnlySourcesCnt = 0; 875 int writableSourcesCnt = 0; 876 Sources sources = Sources.getInstance(EditContactActivity.this); 877 for (EntityDelta delta : mState) { 878 final String accountType = delta.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 879 final ContactsSource contactsSource = sources.getInflatedSource(accountType, 880 ContactsSource.LEVEL_CONSTRAINTS); 881 if (contactsSource != null && contactsSource.readOnly) { 882 readOnlySourcesCnt += 1; 883 } else { 884 writableSourcesCnt += 1; 885 } 886 } 887 888 if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) { 889 showDialog(DIALOG_CONFIRM_READONLY_DELETE); 890 } else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) { 891 showDialog(DIALOG_CONFIRM_READONLY_HIDE); 892 } else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) { 893 showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE); 894 } else { 895 showDialog(DIALOG_CONFIRM_DELETE); 896 } 897 return true; 898 } 899 900 /** 901 * Pick a specific photo to be added under the currently selected tab. 902 */ doPickPhotoAction(long rawContactId)903 boolean doPickPhotoAction(long rawContactId) { 904 if (!hasValidState()) return false; 905 906 try { 907 // Launch picker to choose photo for selected contact 908 final Intent intent = ContactsUtils.getPhotoPickIntent(); 909 startActivityForResult(intent, PHOTO_PICKED_WITH_DATA); 910 mRawContactIdRequestingPhoto = rawContactId; 911 } catch (ActivityNotFoundException e) { 912 Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 913 } 914 return true; 915 } 916 917 /** {@inheritDoc} */ onDeleted(Editor editor)918 public void onDeleted(Editor editor) { 919 // Ignore any editor deletes 920 } 921 doSplitContactAction()922 private boolean doSplitContactAction() { 923 if (!hasValidState()) return false; 924 925 showAndManageDialog(createSplitDialog()); 926 return true; 927 } 928 createSplitDialog()929 private Dialog createSplitDialog() { 930 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 931 builder.setTitle(R.string.splitConfirmation_title); 932 builder.setIcon(android.R.drawable.ic_dialog_alert); 933 builder.setMessage(R.string.splitConfirmation); 934 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 935 public void onClick(DialogInterface dialog, int which) { 936 // Split the contacts 937 mState.splitRawContacts(); 938 doSaveAction(SAVE_MODE_SPLIT); 939 } 940 }); 941 builder.setNegativeButton(android.R.string.cancel, null); 942 builder.setCancelable(false); 943 return builder.create(); 944 } 945 doJoinContactAction()946 private boolean doJoinContactAction() { 947 return doSaveAction(SAVE_MODE_JOIN); 948 } 949 950 951 952 953 954 955 956 957 /** 958 * Build dialog that handles adding a new {@link RawContacts} after the user 959 * picks a specific {@link ContactsSource}. 960 */ 961 private static class AddContactTask extends 962 WeakAsyncTask<Void, Void, AlertDialog.Builder, EditContactActivity> { AddContactTask(EditContactActivity target)963 public AddContactTask(EditContactActivity target) { 964 super(target); 965 } 966 967 @Override doInBackground(final EditContactActivity target, Void... params)968 protected AlertDialog.Builder doInBackground(final EditContactActivity target, 969 Void... params) { 970 final Sources sources = Sources.getInstance(target); 971 972 // Wrap our context to inflate list items using correct theme 973 final Context dialogContext = new ContextThemeWrapper(target, android.R.style.Theme_Light); 974 final LayoutInflater dialogInflater = (LayoutInflater)dialogContext 975 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 976 977 final ArrayList<Account> writable = sources.getAccounts(true); 978 979 // No Accounts available. Create a phone-local contact. 980 if (writable.isEmpty()) { 981 selectAccount(null); 982 return null; // Don't show a dialog. 983 } 984 985 // In the common case of a single account being writable, auto-select 986 // it without showing a dialog. 987 if (writable.size() == 1) { 988 selectAccount(writable.get(0)); 989 return null; // Don't show a dialog. 990 } 991 992 final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(target, 993 android.R.layout.simple_list_item_2, writable) { 994 @Override 995 public View getView(int position, View convertView, ViewGroup parent) { 996 if (convertView == null) { 997 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2, 998 parent, false); 999 } 1000 1001 // TODO: show icon along with title 1002 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 1003 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 1004 1005 final Account account = this.getItem(position); 1006 final ContactsSource source = sources.getInflatedSource(account.type, 1007 ContactsSource.LEVEL_SUMMARY); 1008 1009 text1.setText(account.name); 1010 text2.setText(source.getDisplayLabel(target)); 1011 1012 return convertView; 1013 } 1014 }; 1015 1016 final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { 1017 public void onClick(DialogInterface dialog, int which) { 1018 dialog.dismiss(); 1019 1020 // Create new contact based on selected source 1021 final Account account = accountAdapter.getItem(which); 1022 selectAccount(account); 1023 1024 // Update the UI. 1025 EditContactActivity target = mTarget.get(); 1026 if (target != null) { 1027 target.bindEditors(); 1028 } 1029 } 1030 }; 1031 1032 final DialogInterface.OnCancelListener cancelListener = new DialogInterface.OnCancelListener() { 1033 public void onCancel(DialogInterface dialog) { 1034 // If nothing remains, close activity 1035 if (!target.hasValidState()) { 1036 target.finish(); 1037 } 1038 } 1039 }; 1040 1041 // TODO: when canceled and was single add, finish() 1042 final AlertDialog.Builder builder = new AlertDialog.Builder(target); 1043 builder.setTitle(R.string.dialog_new_contact_account); 1044 builder.setSingleChoiceItems(accountAdapter, 0, clickListener); 1045 builder.setOnCancelListener(cancelListener); 1046 return builder; 1047 } 1048 1049 /** 1050 * Sets up EditContactActivity's mState for the account selected. 1051 * Runs from a background thread. 1052 * 1053 * @param account may be null to signal a device-local contact should 1054 * be created. 1055 */ selectAccount(Account account)1056 private void selectAccount(Account account) { 1057 EditContactActivity target = mTarget.get(); 1058 if (target == null) { 1059 return; 1060 } 1061 final Sources sources = Sources.getInstance(target); 1062 final ContentValues values = new ContentValues(); 1063 if (account != null) { 1064 values.put(RawContacts.ACCOUNT_NAME, account.name); 1065 values.put(RawContacts.ACCOUNT_TYPE, account.type); 1066 } else { 1067 values.putNull(RawContacts.ACCOUNT_NAME); 1068 values.putNull(RawContacts.ACCOUNT_TYPE); 1069 } 1070 1071 // Parse any values from incoming intent 1072 final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values)); 1073 final ContactsSource source = sources.getInflatedSource( 1074 account != null ? account.type : null, 1075 ContactsSource.LEVEL_CONSTRAINTS); 1076 final Bundle extras = target.getIntent().getExtras(); 1077 EntityModifier.parseExtras(target, source, insert, extras); 1078 1079 // Ensure we have some default fields 1080 EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE); 1081 EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE); 1082 1083 // Create "My Contacts" membership for Google contacts 1084 // TODO: move this off into "templates" for each given source 1085 if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) { 1086 GoogleSource.attemptMyContactsMembership(insert, target); 1087 } 1088 1089 // TODO: no synchronization here on target.mState. This 1090 // runs in the background thread, but it's accessed from 1091 // multiple thread, including the UI thread. 1092 if (target.mState == null) { 1093 // Create state if none exists yet 1094 target.mState = EntitySet.fromSingle(insert); 1095 } else { 1096 // Add contact onto end of existing state 1097 target.mState.add(insert); 1098 } 1099 } 1100 1101 @Override onPostExecute(EditContactActivity target, AlertDialog.Builder result)1102 protected void onPostExecute(EditContactActivity target, AlertDialog.Builder result) { 1103 if (result != null) { 1104 // Note: null is returned when no dialog is to be 1105 // shown (no multiple accounts to select between) 1106 target.showAndManageDialog(result.create()); 1107 } else { 1108 // Account was auto-selected on the background thread, 1109 // but we need to update the UI still in the 1110 // now-current UI thread. 1111 target.bindEditors(); 1112 } 1113 } 1114 } 1115 1116 1117 createDeleteDialog()1118 private Dialog createDeleteDialog() { 1119 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 1120 builder.setTitle(R.string.deleteConfirmation_title); 1121 builder.setIcon(android.R.drawable.ic_dialog_alert); 1122 builder.setMessage(R.string.deleteConfirmation); 1123 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 1124 public void onClick(DialogInterface dialog, int which) { 1125 // Mark all raw contacts for deletion 1126 for (EntityDelta delta : mState) { 1127 delta.markDeleted(); 1128 } 1129 1130 // Save the deletes 1131 doSaveAction(SAVE_MODE_DEFAULT); 1132 finish(); 1133 } 1134 }); 1135 builder.setNegativeButton(android.R.string.cancel, null); 1136 builder.setCancelable(false); 1137 return builder.create(); 1138 } 1139 1140 /** 1141 * Create dialog for selecting primary display name. 1142 */ createNameDialog()1143 private Dialog createNameDialog() { 1144 // Build set of all available display names 1145 final ArrayList<ValuesDelta> allNames = Lists.newArrayList(); 1146 for (EntityDelta entity : mState) { 1147 final ArrayList<ValuesDelta> displayNames = entity 1148 .getMimeEntries(StructuredName.CONTENT_ITEM_TYPE); 1149 allNames.addAll(displayNames); 1150 } 1151 1152 // Wrap our context to inflate list items using correct theme 1153 final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); 1154 final LayoutInflater dialogInflater = this.getLayoutInflater() 1155 .cloneInContext(dialogContext); 1156 1157 final ListAdapter nameAdapter = new ArrayAdapter<ValuesDelta>(this, 1158 android.R.layout.simple_list_item_1, allNames) { 1159 @Override 1160 public View getView(int position, View convertView, ViewGroup parent) { 1161 if (convertView == null) { 1162 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1, 1163 parent, false); 1164 } 1165 1166 final ValuesDelta structuredName = this.getItem(position); 1167 final String displayName = structuredName.getAsString(StructuredName.DISPLAY_NAME); 1168 1169 ((TextView)convertView).setText(displayName); 1170 1171 return convertView; 1172 } 1173 }; 1174 1175 final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { 1176 public void onClick(DialogInterface dialog, int which) { 1177 dialog.dismiss(); 1178 1179 // User picked display name, so make super-primary 1180 final ValuesDelta structuredName = allNames.get(which); 1181 structuredName.put(Data.IS_PRIMARY, 1); 1182 structuredName.put(Data.IS_SUPER_PRIMARY, 1); 1183 } 1184 }; 1185 1186 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 1187 builder.setTitle(R.string.dialog_primary_name); 1188 builder.setSingleChoiceItems(nameAdapter, 0, clickListener); 1189 return builder.create(); 1190 } 1191 1192 /** 1193 * Compare EntityDeltas for sorting the stack of editors. 1194 */ compare(EntityDelta one, EntityDelta two)1195 public int compare(EntityDelta one, EntityDelta two) { 1196 // Check direct equality 1197 if (one.equals(two)) { 1198 return 0; 1199 } 1200 1201 final Sources sources = Sources.getInstance(this); 1202 String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1203 final ContactsSource oneSource = sources.getInflatedSource(accountType, 1204 ContactsSource.LEVEL_SUMMARY); 1205 accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1206 final ContactsSource twoSource = sources.getInflatedSource(accountType, 1207 ContactsSource.LEVEL_SUMMARY); 1208 1209 // Check read-only 1210 if (oneSource.readOnly && !twoSource.readOnly) { 1211 return 1; 1212 } else if (twoSource.readOnly && !oneSource.readOnly) { 1213 return -1; 1214 } 1215 1216 // Check account type 1217 boolean skipAccountTypeCheck = false; 1218 boolean oneIsGoogle = oneSource instanceof GoogleSource; 1219 boolean twoIsGoogle = twoSource instanceof GoogleSource; 1220 if (oneIsGoogle && !twoIsGoogle) { 1221 return -1; 1222 } else if (twoIsGoogle && !oneIsGoogle) { 1223 return 1; 1224 } else { 1225 skipAccountTypeCheck = true; 1226 } 1227 1228 int value; 1229 if (!skipAccountTypeCheck) { 1230 value = oneSource.accountType.compareTo(twoSource.accountType); 1231 if (value != 0) { 1232 return value; 1233 } 1234 } 1235 1236 // Check account name 1237 ValuesDelta oneValues = one.getValues(); 1238 String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME); 1239 if (oneAccount == null) oneAccount = ""; 1240 ValuesDelta twoValues = two.getValues(); 1241 String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME); 1242 if (twoAccount == null) twoAccount = ""; 1243 value = oneAccount.compareTo(twoAccount); 1244 if (value != 0) { 1245 return value; 1246 } 1247 1248 // Both are in the same account, fall back to contact ID 1249 long oneId = oneValues.getAsLong(RawContacts._ID); 1250 long twoId = twoValues.getAsLong(RawContacts._ID); 1251 return (int)(oneId - twoId); 1252 } 1253 } 1254