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