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