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