ConfirmAddDetailActivity.java revision a84cf9274905479f09eb01dd3e7c6fbc2c5c6145
1/* 2 * Copyright (C) 2011 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 17package com.android.contacts.activities; 18 19import com.android.contacts.R; 20import com.android.contacts.editor.Editor; 21import com.android.contacts.editor.ViewIdGenerator; 22import com.android.contacts.model.AccountType; 23import com.android.contacts.model.AccountTypeManager; 24import com.android.contacts.model.DataKind; 25import com.android.contacts.model.EntityDelta; 26import com.android.contacts.model.EntityDelta.ValuesDelta; 27import com.android.contacts.model.EntityDeltaList; 28import com.android.contacts.model.EntityModifier; 29import com.android.contacts.util.DialogManager; 30import com.android.contacts.util.EmptyService; 31 32import android.app.Activity; 33import android.app.Dialog; 34import android.app.ProgressDialog; 35import android.content.AsyncQueryHandler; 36import android.content.ContentProviderOperation; 37import android.content.ContentProviderResult; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.Context; 41import android.content.Intent; 42import android.content.OperationApplicationException; 43import android.database.Cursor; 44import android.graphics.Bitmap; 45import android.graphics.BitmapFactory; 46import android.net.Uri; 47import android.net.Uri.Builder; 48import android.os.AsyncTask; 49import android.os.Bundle; 50import android.os.RemoteException; 51import android.provider.ContactsContract; 52import android.provider.ContactsContract.CommonDataKinds.Email; 53import android.provider.ContactsContract.CommonDataKinds.Im; 54import android.provider.ContactsContract.CommonDataKinds.Nickname; 55import android.provider.ContactsContract.CommonDataKinds.Phone; 56import android.provider.ContactsContract.CommonDataKinds.Photo; 57import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 58import android.provider.ContactsContract.Contacts; 59import android.provider.ContactsContract.Data; 60import android.provider.ContactsContract.RawContacts; 61import android.provider.ContactsContract.RawContactsEntity; 62import android.telephony.PhoneNumberUtils; 63import android.text.TextUtils; 64import android.util.Log; 65import android.view.LayoutInflater; 66import android.view.View; 67import android.view.View.OnClickListener; 68import android.view.ViewGroup; 69import android.widget.ImageView; 70import android.widget.TextView; 71import android.widget.Toast; 72 73import java.lang.ref.WeakReference; 74import java.util.ArrayList; 75import java.util.HashMap; 76 77/** 78 * This is a dialog-themed activity for confirming the addition of a detail to an existing contact 79 * (once the user has selected this contact from a list of all contacts). The incoming intent 80 * must have an extra with max 1 phone or email specified, using 81 * {@link android.provider.ContactsContract.Intents.Insert#PHONE} with type 82 * {@link android.provider.ContactsContract.Intents.Insert#PHONE_TYPE} or 83 * {@link android.provider.ContactsContract.Intents.Insert#EMAIL} with type 84 * {@link android.provider.ContactsContract.Intents.Insert#EMAIL_TYPE} intent keys. 85 */ 86public class ConfirmAddDetailActivity extends Activity implements 87 DialogManager.DialogShowingViewActivity { 88 89 private static final String TAG = "ConfirmAdd"; // The class name is too long to be a tag. 90 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 91 92 private LayoutInflater mInflater; 93 private View mRootView; 94 private TextView mDisplayNameView; 95 private TextView mReadOnlyWarningView; 96 private ImageView mPhotoView; 97 private ViewGroup mEditorContainerView; 98 private static WeakReference<ProgressDialog> sProgressDialog; 99 100 private AccountTypeManager mAccountTypeManager; 101 private ContentResolver mContentResolver; 102 103 private AccountType mEditableAccountType; 104 private Uri mContactUri; 105 private long mContactId; 106 private String mDisplayName; 107 private boolean mIsReadOnly; 108 109 private QueryHandler mQueryHandler; 110 111 /** {@link EntityDeltaList} for the entire selected contact. */ 112 private EntityDeltaList mEntityDeltaList; 113 114 /** {@link EntityDeltaList} for the editable account */ 115 private EntityDelta mEntityDelta; 116 117 private String mMimetype = Phone.CONTENT_ITEM_TYPE; 118 119 /** 120 * DialogManager may be needed if the user wants to apply a "custom" label to the contact detail 121 */ 122 private final DialogManager mDialogManager = new DialogManager(this); 123 124 /** 125 * PhotoQuery contains the projection used for retrieving the name and photo 126 * ID of a contact. 127 */ 128 private interface ContactQuery { 129 final String[] COLUMNS = new String[] { 130 Contacts._ID, 131 Contacts.LOOKUP_KEY, 132 Contacts.PHOTO_ID, 133 Contacts.DISPLAY_NAME, 134 }; 135 final int _ID = 0; 136 final int LOOKUP_KEY = 1; 137 final int PHOTO_ID = 2; 138 final int DISPLAY_NAME = 3; 139 } 140 141 /** 142 * PhotoQuery contains the projection used for retrieving the raw bytes of 143 * the contact photo. 144 */ 145 private interface PhotoQuery { 146 final String[] COLUMNS = new String[] { 147 Photo.PHOTO 148 }; 149 150 final int PHOTO = 0; 151 } 152 153 /** 154 * ExtraInfoQuery contains the projection used for retrieving the extra info 155 * on a contact (only needed if someone else exists with the same name as 156 * this contact). 157 */ 158 private interface ExtraInfoQuery { 159 final String[] COLUMNS = new String[] { 160 RawContacts.CONTACT_ID, 161 Data.MIMETYPE, 162 Data.DATA1, 163 }; 164 final int CONTACT_ID = 0; 165 final int MIMETYPE = 1; 166 final int DATA1 = 2; 167 } 168 169 /** 170 * List of mimetypes to use in order of priority to display for a contact in 171 * a disambiguation case. For example, if the contact does not have a 172 * nickname, use the email field, and etc. 173 */ 174 private static final String[] MIME_TYPE_PRIORITY_LIST = new String[] { 175 Nickname.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, 176 StructuredPostal.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE }; 177 178 private static final int TOKEN_CONTACT_INFO = 0; 179 private static final int TOKEN_PHOTO_QUERY = 1; 180 private static final int TOKEN_DISAMBIGUATION_QUERY = 2; 181 private static final int TOKEN_EXTRA_INFO_QUERY = 3; 182 183 private final OnClickListener mDetailsButtonClickListener = new OnClickListener() { 184 @Override 185 public void onClick(View v) { 186 if (mIsReadOnly) { 187 onSaveCompleted(true); 188 } else { 189 doSaveAction(); 190 } 191 } 192 }; 193 194 private final OnClickListener mDoneButtonClickListener = new OnClickListener() { 195 @Override 196 public void onClick(View v) { 197 doSaveAction(); 198 } 199 }; 200 201 private final OnClickListener mCancelButtonClickListener = new OnClickListener() { 202 @Override 203 public void onClick(View v) { 204 setResult(RESULT_CANCELED); 205 finish(); 206 } 207 }; 208 209 @Override 210 protected void onCreate(Bundle icicle) { 211 super.onCreate(icicle); 212 213 mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 214 mContentResolver = getContentResolver(); 215 216 final Intent intent = getIntent(); 217 mContactUri = intent.getData(); 218 219 if (mContactUri == null) { 220 setResult(RESULT_CANCELED); 221 finish(); 222 } 223 224 Bundle extras = intent.getExtras(); 225 if (extras != null) { 226 if (extras.containsKey(ContactsContract.Intents.Insert.PHONE)) { 227 mMimetype = Phone.CONTENT_ITEM_TYPE; 228 } else if (extras.containsKey(ContactsContract.Intents.Insert.EMAIL)) { 229 mMimetype = Email.CONTENT_ITEM_TYPE; 230 } else { 231 throw new IllegalStateException("Error: No valid mimetype found in intent extras"); 232 } 233 } 234 235 mAccountTypeManager = AccountTypeManager.getInstance(this); 236 237 setContentView(R.layout.confirm_add_detail_activity); 238 239 mRootView = findViewById(R.id.root_view); 240 mReadOnlyWarningView = (TextView) findViewById(R.id.read_only_warning); 241 242 // Setup "header" (containing contact info) to save the detail and then go to the editor 243 findViewById(R.id.open_details_push_layer).setOnClickListener(mDetailsButtonClickListener); 244 245 // Setup "done" button to save the detail to the contact and exit. 246 findViewById(R.id.btn_done).setOnClickListener(mDoneButtonClickListener); 247 248 // Setup "cancel" button to return to previous activity. 249 findViewById(R.id.btn_cancel).setOnClickListener(mCancelButtonClickListener); 250 251 // Retrieve references to all the Views in the dialog activity. 252 mDisplayNameView = (TextView) findViewById(R.id.name); 253 mPhotoView = (ImageView) findViewById(R.id.photo); 254 mEditorContainerView = (ViewGroup) findViewById(R.id.editor_container); 255 256 resetAsyncQueryHandler(); 257 startContactQuery(mContactUri); 258 259 new QueryEntitiesTask(this).execute(intent); 260 } 261 262 @Override 263 public DialogManager getDialogManager() { 264 return mDialogManager; 265 } 266 267 @Override 268 protected Dialog onCreateDialog(int id, Bundle args) { 269 if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args); 270 271 // Nobody knows about the Dialog 272 Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args); 273 return null; 274 } 275 276 /** 277 * Reset the query handler by creating a new QueryHandler instance. 278 */ 279 private void resetAsyncQueryHandler() { 280 // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really 281 // need the old async queries to be cancelled, let's do it the hard way. 282 mQueryHandler = new QueryHandler(mContentResolver); 283 } 284 285 /** 286 * Internal method to query contact by Uri. 287 * 288 * @param contactUri the contact uri 289 */ 290 private void startContactQuery(Uri contactUri) { 291 mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS, 292 null, null, null); 293 } 294 295 /** 296 * Internal method to query contact photo by photo id and uri. 297 * 298 * @param photoId the photo id. 299 * @param lookupKey the lookup uri. 300 */ 301 private void startPhotoQuery(long photoId, Uri lookupKey) { 302 mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey, 303 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), 304 PhotoQuery.COLUMNS, null, null, null); 305 } 306 307 /** 308 * Internal method to query for contacts with a given display name. 309 * 310 * @param contactDisplayName the display name to look for. 311 */ 312 private void startDisambiguationQuery(String contactDisplayName) { 313 // Apply a limit of 1 result to the query because we only need to 314 // determine whether or not at least one other contact has the same 315 // name. We don't need to find ALL other contacts with the same name. 316 final Builder builder = Contacts.CONTENT_URI.buildUpon(); 317 builder.appendQueryParameter("limit", String.valueOf(1)); 318 final Uri uri = builder.build(); 319 320 final String displayNameSelection; 321 final String[] selectionArgs; 322 if (TextUtils.isEmpty(contactDisplayName)) { 323 displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " IS NULL"; 324 selectionArgs = new String[] { String.valueOf(mContactId) }; 325 } else { 326 displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " = ?"; 327 selectionArgs = new String[] { contactDisplayName, String.valueOf(mContactId) }; 328 } 329 mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri, 330 new String[] { Contacts._ID } /* unused projection but a valid one was needed */, 331 displayNameSelection + " AND " + Contacts.PHOTO_ID + " IS NULL AND " 332 + Contacts._ID + " <> ?", selectionArgs, null); 333 } 334 335 /** 336 * Internal method to query for extra data fields for this contact. 337 */ 338 private void startExtraInfoQuery() { 339 mQueryHandler.startQuery(TOKEN_EXTRA_INFO_QUERY, null, Data.CONTENT_URI, 340 ExtraInfoQuery.COLUMNS, RawContacts.CONTACT_ID + " = ?", 341 new String[] { String.valueOf(mContactId) }, null); 342 } 343 344 private static class QueryEntitiesTask extends AsyncTask<Intent, Void, EntityDeltaList> { 345 346 private ConfirmAddDetailActivity activityTarget; 347 private String mSelection; 348 349 public QueryEntitiesTask(ConfirmAddDetailActivity target) { 350 activityTarget = target; 351 } 352 353 @Override 354 protected EntityDeltaList doInBackground(Intent... params) { 355 356 final Intent intent = params[0]; 357 358 final ContentResolver resolver = activityTarget.getContentResolver(); 359 360 // Handle both legacy and new authorities 361 final Uri data = intent.getData(); 362 final String authority = data.getAuthority(); 363 final String mimeType = intent.resolveType(resolver); 364 365 mSelection = "0"; 366 String selectionArg = null; 367 if (ContactsContract.AUTHORITY.equals(authority)) { 368 if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) { 369 // Handle selected aggregate 370 final long contactId = ContentUris.parseId(data); 371 selectionArg = String.valueOf(contactId); 372 mSelection = RawContacts.CONTACT_ID + "=?"; 373 } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) { 374 final long rawContactId = ContentUris.parseId(data); 375 final long contactId = queryForContactId(resolver, rawContactId); 376 selectionArg = String.valueOf(contactId); 377 mSelection = RawContacts.CONTACT_ID + "=?"; 378 } 379 } else if (android.provider.Contacts.AUTHORITY.equals(authority)) { 380 final long rawContactId = ContentUris.parseId(data); 381 selectionArg = String.valueOf(rawContactId); 382 mSelection = Data.RAW_CONTACT_ID + "=?"; 383 } 384 385 // Note that this query does not need to concern itself with whether the contact is 386 // the user's profile, since the profile does not show up in the picker. 387 return EntityDeltaList.fromQuery(RawContactsEntity.CONTENT_URI, 388 activityTarget.getContentResolver(), mSelection, 389 new String[] { selectionArg }, null); 390 } 391 392 private static long queryForContactId(ContentResolver resolver, long rawContactId) { 393 Cursor contactIdCursor = null; 394 long contactId = -1; 395 try { 396 contactIdCursor = resolver.query(RawContacts.CONTENT_URI, 397 new String[] { RawContacts.CONTACT_ID }, 398 RawContacts._ID + "=?", new String[] { String.valueOf(rawContactId) }, 399 null); 400 if (contactIdCursor != null && contactIdCursor.moveToFirst()) { 401 contactId = contactIdCursor.getLong(0); 402 } 403 } finally { 404 if (contactIdCursor != null) { 405 contactIdCursor.close(); 406 } 407 } 408 return contactId; 409 } 410 411 @Override 412 protected void onPostExecute(EntityDeltaList entityList) { 413 if (activityTarget.isFinishing()) { 414 return; 415 } 416 activityTarget.setEntityDeltaList(entityList); 417 } 418 } 419 420 private class QueryHandler extends AsyncQueryHandler { 421 422 public QueryHandler(ContentResolver cr) { 423 super(cr); 424 } 425 426 @Override 427 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 428 try { 429 if (this != mQueryHandler) { 430 Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!"); 431 return; 432 } 433 if (ConfirmAddDetailActivity.this.isFinishing()) { 434 return; 435 } 436 437 switch (token) { 438 case TOKEN_PHOTO_QUERY: { 439 // Set the photo 440 Bitmap photoBitmap = null; 441 if (cursor != null && cursor.moveToFirst() 442 && !cursor.isNull(PhotoQuery.PHOTO)) { 443 byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO); 444 photoBitmap = BitmapFactory.decodeByteArray(photoData, 0, 445 photoData.length, null); 446 } 447 448 if (photoBitmap != null) { 449 mPhotoView.setImageBitmap(photoBitmap); 450 } 451 452 break; 453 } 454 case TOKEN_CONTACT_INFO: { 455 // Set the contact's name 456 if (cursor != null && cursor.moveToFirst()) { 457 // Get the cursor values 458 mDisplayName = cursor.getString(ContactQuery.DISPLAY_NAME); 459 final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); 460 461 // If there is no photo ID, then do a disambiguation 462 // query because other contacts could have the same 463 // name as this contact. 464 if (photoId == 0) { 465 mContactId = cursor.getLong(ContactQuery._ID); 466 startDisambiguationQuery(mDisplayName); 467 } else { 468 // Otherwise do the photo query. 469 Uri lookupUri = Contacts.getLookupUri(mContactId, 470 cursor.getString(ContactQuery.LOOKUP_KEY)); 471 startPhotoQuery(photoId, lookupUri); 472 // Display the name because there is no 473 // disambiguation query. 474 setDisplayName(); 475 showDialogContent(); 476 } 477 } 478 break; 479 } 480 case TOKEN_DISAMBIGUATION_QUERY: { 481 // If a cursor was returned with more than 0 results, 482 // then at least one other contact exists with the same 483 // name as this contact. Extra info on this contact must 484 // be displayed to disambiguate the contact, so retrieve 485 // those additional fields. Otherwise, no other contacts 486 // with this name exists, so do nothing further. 487 if (cursor != null && cursor.getCount() > 0) { 488 startExtraInfoQuery(); 489 } else { 490 // If there are no other contacts with this name, 491 // then display the name. 492 setDisplayName(); 493 showDialogContent(); 494 } 495 break; 496 } 497 case TOKEN_EXTRA_INFO_QUERY: { 498 // This case should only occur if there are one or more 499 // other contacts with the same contact name. 500 if (cursor != null && cursor.moveToFirst()) { 501 HashMap<String, String> hashMapCursorData = new 502 HashMap<String, String>(); 503 504 // Convert the cursor data into a hashmap of 505 // (mimetype, data value) pairs. If a contact has 506 // multiple values with the same mimetype, it's fine 507 // to override that hashmap entry because we only 508 // need one value of that type. 509 while (!cursor.isAfterLast()) { 510 final String mimeType = cursor.getString(ExtraInfoQuery.MIMETYPE); 511 if (!TextUtils.isEmpty(mimeType)) { 512 String value = cursor.getString(ExtraInfoQuery.DATA1); 513 if (!TextUtils.isEmpty(value)) { 514 // As a special case, phone numbers 515 // should be formatted in a specific way. 516 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { 517 value = PhoneNumberUtils.formatNumber(value); 518 } 519 hashMapCursorData.put(mimeType, value); 520 } 521 } 522 cursor.moveToNext(); 523 } 524 525 // Find the first non-empty field according to the 526 // mimetype priority list and display this under the 527 // contact's display name to disambiguate the contact. 528 for (String mimeType : MIME_TYPE_PRIORITY_LIST) { 529 if (hashMapCursorData.containsKey(mimeType)) { 530 setDisplayName(); 531 setExtraInfoField(hashMapCursorData.get(mimeType)); 532 break; 533 } 534 } 535 showDialogContent(); 536 } 537 break; 538 } 539 } 540 } finally { 541 if (cursor != null) { 542 cursor.close(); 543 } 544 } 545 } 546 } 547 548 private void setEntityDeltaList(EntityDeltaList entityList) { 549 if (entityList == null) { 550 throw new IllegalStateException(); 551 } 552 if (VERBOSE_LOGGING) { 553 Log.v(TAG, "setEntityDeltaList: " + entityList); 554 } 555 556 mEntityDeltaList = entityList; 557 558 // Find the editable type. 559 mEntityDelta = mEntityDeltaList.getFirstWritableRawContact(this); 560 if (mEntityDelta == null) { 561 mIsReadOnly = true; 562 mEditableAccountType = null; 563 } else { 564 mIsReadOnly = false; 565 566 mEditableAccountType = mEntityDelta.getRawContactAccountType(this); 567 568 // Handle any incoming values that should be inserted 569 final Bundle extras = getIntent().getExtras(); 570 if (extras != null && extras.size() > 0) { 571 // If there are any intent extras, add them as additional fields in the EntityDelta. 572 EntityModifier.parseExtras(this, mEditableAccountType, mEntityDelta, extras); 573 } 574 } 575 576 bindEditor(); 577 } 578 579 /** 580 * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object. 581 */ 582 private void bindEditor() { 583 if (mEntityDeltaList == null) { 584 throw new IllegalStateException(); 585 } 586 587 // If no valid raw contact (to insert the data) was found, we won't have an editable 588 // account type to use. In this case, display an error message and hide the "OK" button. 589 if (mIsReadOnly) { 590 mReadOnlyWarningView.setText(getString(R.string.contact_read_only)); 591 mReadOnlyWarningView.setVisibility(View.VISIBLE); 592 mEditorContainerView.setVisibility(View.GONE); 593 findViewById(R.id.btn_done).setVisibility(View.GONE); 594 // Nothing more to be done, just show the UI 595 showDialogContent(); 596 return; 597 } 598 599 // Otherwise display an editor that allows the user to add the data to this raw contact. 600 for (DataKind kind : mEditableAccountType.getSortedDataKinds()) { 601 // Skip kind that are not editable 602 if (!kind.editable) continue; 603 if (mMimetype.equals(kind.mimeType)) { 604 for (ValuesDelta valuesDelta : mEntityDelta.getMimeEntries(mMimetype)) { 605 // Skip entries that aren't visible 606 if (!valuesDelta.isVisible()) continue; 607 if (valuesDelta.isInsert()) { 608 inflateEditorView(kind, valuesDelta, mEntityDelta); 609 return; 610 } 611 } 612 } 613 } 614 } 615 616 /** 617 * Creates an EditorView for the given entry. This function must be used while constructing 618 * the views corresponding to the the object-model. The resulting EditorView is also added 619 * to the end of mEditors 620 */ 621 private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, EntityDelta state) { 622 final View view = mInflater.inflate(dataKind.editorLayoutResourceId, mEditorContainerView, 623 false); 624 625 if (view instanceof Editor) { 626 Editor editor = (Editor) view; 627 // Don't allow deletion of the field because there is only 1 detail in this editor. 628 editor.setDeletable(false); 629 editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator()); 630 } 631 632 mEditorContainerView.addView(view); 633 } 634 635 /** 636 * Set the display name to the correct TextView. Don't do this until it is 637 * certain there is no need for a disambiguation field (otherwise the screen 638 * will flicker because the name will be centered and then moved upwards). 639 */ 640 private void setDisplayName() { 641 mDisplayNameView.setText(mDisplayName); 642 } 643 644 /** 645 * Set the TextView (for extra contact info) with the given value and make the 646 * TextView visible. 647 */ 648 private void setExtraInfoField(String value) { 649 TextView extraTextView = (TextView) findViewById(R.id.extra_info); 650 extraTextView.setVisibility(View.VISIBLE); 651 extraTextView.setText(value); 652 } 653 654 /** 655 * Shows all the contents of the dialog to the user at one time. This should only be called 656 * once all the queries have completed, otherwise the screen will flash as additional data 657 * comes in. 658 */ 659 private void showDialogContent() { 660 mRootView.setVisibility(View.VISIBLE); 661 } 662 663 /** 664 * Saves or creates the contact based on the mode, and if successful 665 * finishes the activity. 666 */ 667 private void doSaveAction() { 668 final PersistTask task = new PersistTask(this, mAccountTypeManager); 669 task.execute(mEntityDeltaList); 670 } 671 672 /** 673 * Background task for persisting edited contact data, using the changes 674 * defined by a set of {@link EntityDelta}. This task starts 675 * {@link EmptyService} to make sure the background thread can finish 676 * persisting in cases where the system wants to reclaim our process. 677 */ 678 private static class PersistTask extends AsyncTask<EntityDeltaList, Void, Integer> { 679 // In the future, use ContactSaver instead of WeakAsyncTask because of 680 // the danger of the activity being null during a save action 681 private static final int PERSIST_TRIES = 3; 682 683 private static final int RESULT_UNCHANGED = 0; 684 private static final int RESULT_SUCCESS = 1; 685 private static final int RESULT_FAILURE = 2; 686 687 private ConfirmAddDetailActivity activityTarget; 688 689 private AccountTypeManager mAccountTypeManager; 690 691 public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) { 692 activityTarget = target; 693 mAccountTypeManager = accountTypeManager; 694 } 695 696 @Override 697 protected void onPreExecute() { 698 sProgressDialog = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget, 699 null, activityTarget.getText(R.string.savingContact))); 700 701 // Before starting this task, start an empty service to protect our 702 // process from being reclaimed by the system. 703 final Context context = activityTarget; 704 context.startService(new Intent(context, EmptyService.class)); 705 } 706 707 @Override 708 protected Integer doInBackground(EntityDeltaList... params) { 709 final Context context = activityTarget; 710 final ContentResolver resolver = context.getContentResolver(); 711 712 EntityDeltaList state = params[0]; 713 714 if (state == null) { 715 return RESULT_FAILURE; 716 } 717 718 // Trim any empty fields, and RawContacts, before persisting 719 EntityModifier.trimEmpty(state, mAccountTypeManager); 720 721 // Attempt to persist changes 722 int tries = 0; 723 Integer result = RESULT_FAILURE; 724 while (tries++ < PERSIST_TRIES) { 725 try { 726 // Build operations and try applying 727 final ArrayList<ContentProviderOperation> diff = state.buildDiff(); 728 ContentProviderResult[] results = null; 729 if (!diff.isEmpty()) { 730 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff); 731 } 732 733 result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED; 734 break; 735 736 } catch (RemoteException e) { 737 // Something went wrong, bail without success 738 Log.e(TAG, "Problem persisting user edits", e); 739 break; 740 741 } catch (OperationApplicationException e) { 742 // Version consistency failed, bail without success 743 Log.e(TAG, "Version consistency failed", e); 744 break; 745 } 746 } 747 748 return result; 749 } 750 751 /** {@inheritDoc} */ 752 @Override 753 protected void onPostExecute(Integer result) { 754 final Context context = activityTarget; 755 756 dismissProgressDialog(); 757 758 // Show a toast message based on the success or failure of the save action. 759 if (result == RESULT_SUCCESS) { 760 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 761 } else if (result == RESULT_FAILURE) { 762 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 763 } 764 765 // Stop the service that was protecting us 766 context.stopService(new Intent(context, EmptyService.class)); 767 activityTarget.onSaveCompleted(result != RESULT_FAILURE); 768 } 769 } 770 771 @Override 772 protected void onStop() { 773 super.onStop(); 774 // Dismiss the progress dialog here to prevent leaking the window on orientation change. 775 dismissProgressDialog(); 776 } 777 778 /** 779 * Dismiss the progress dialog (check if it is null because it is a {@link WeakReference}). 780 */ 781 private static void dismissProgressDialog() { 782 ProgressDialog dialog = (sProgressDialog == null) ? null : sProgressDialog.get(); 783 if (dialog != null) { 784 dialog.dismiss(); 785 } 786 sProgressDialog = null; 787 } 788 789 /** 790 * This method is intended to be executed after the background task for saving edited info has 791 * finished. The method sets the activity result (and intent if applicable) and finishes the 792 * activity. 793 * @param success is true if the save task completed successfully, or false otherwise. 794 */ 795 private void onSaveCompleted(boolean success) { 796 if (success) { 797 Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri); 798 setResult(RESULT_OK, intent); 799 } else { 800 setResult(RESULT_CANCELED); 801 } 802 finish(); 803 } 804}