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