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