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