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