Home | History | Annotate | Download | only in activities
      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 if (TextUtils.isEmpty(mLookupKey)) {
    499                                 finish();
    500                                 return;
    501                             } else {
    502                                 // Otherwise do the photo query.
    503                                 Uri lookupUri = Contacts.getLookupUri(mContactId, mLookupKey);
    504                                 startPhotoQuery(photoId, lookupUri);
    505                                 // Display the name because there is no
    506                                 // disambiguation query.
    507                                 setDisplayName();
    508                                 showDialogContent();
    509                             }
    510                         }
    511                         break;
    512                     }
    513                     case TOKEN_DISAMBIGUATION_QUERY: {
    514                         // If a cursor was returned with more than 0 results,
    515                         // then at least one other contact exists with the same
    516                         // name as this contact. Extra info on this contact must
    517                         // be displayed to disambiguate the contact, so retrieve
    518                         // those additional fields. Otherwise, no other contacts
    519                         // with this name exists, so do nothing further.
    520                         if (cursor != null && cursor.getCount() > 0) {
    521                             startExtraInfoQuery();
    522                         } else {
    523                             // If there are no other contacts with this name,
    524                             // then display the name.
    525                             setDisplayName();
    526                             showDialogContent();
    527                         }
    528                         break;
    529                     }
    530                     case TOKEN_EXTRA_INFO_QUERY: {
    531                         // This case should only occur if there are one or more
    532                         // other contacts with the same contact name.
    533                         if (cursor != null && cursor.moveToFirst()) {
    534                             HashMap<String, String> hashMapCursorData = new
    535                                     HashMap<String, String>();
    536 
    537                             // Convert the cursor data into a hashmap of
    538                             // (mimetype, data value) pairs. If a contact has
    539                             // multiple values with the same mimetype, it's fine
    540                             // to override that hashmap entry because we only
    541                             // need one value of that type.
    542                             while (!cursor.isAfterLast()) {
    543                                 final String mimeType = cursor.getString(ExtraInfoQuery.MIMETYPE);
    544                                 if (!TextUtils.isEmpty(mimeType)) {
    545                                     String value = cursor.getString(ExtraInfoQuery.DATA1);
    546                                     if (!TextUtils.isEmpty(value)) {
    547                                         // As a special case, phone numbers
    548                                         // should be formatted in a specific way.
    549                                         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
    550                                             value = PhoneNumberUtils.formatNumber(value);
    551                                         }
    552                                         hashMapCursorData.put(mimeType, value);
    553                                     }
    554                                 }
    555                                 cursor.moveToNext();
    556                             }
    557 
    558                             // Find the first non-empty field according to the
    559                             // mimetype priority list and display this under the
    560                             // contact's display name to disambiguate the contact.
    561                             for (String mimeType : MIME_TYPE_PRIORITY_LIST) {
    562                                 if (hashMapCursorData.containsKey(mimeType)) {
    563                                     setDisplayName();
    564                                     setExtraInfoField(hashMapCursorData.get(mimeType));
    565                                     break;
    566                                 }
    567                             }
    568                             showDialogContent();
    569                         }
    570                         break;
    571                     }
    572                 }
    573             } finally {
    574                 if (cursor != null) {
    575                     cursor.close();
    576                 }
    577             }
    578         }
    579     }
    580 
    581     private void setEntityDeltaList(RawContactDeltaList entityList) {
    582         if (entityList == null) {
    583             throw new IllegalStateException();
    584         }
    585         if (VERBOSE_LOGGING) {
    586             Log.v(TAG, "setEntityDeltaList: " + entityList);
    587         }
    588 
    589         mEntityDeltaList = entityList;
    590 
    591         // Find the editable raw_contact.
    592         mRawContactDelta = mEntityDeltaList.getFirstWritableRawContact(this);
    593 
    594         // If no editable raw_contacts are found, create one.
    595         if (mRawContactDelta == null) {
    596             mRawContactDelta = addEditableRawContact(this, mEntityDeltaList);
    597 
    598             if ((mRawContactDelta != null) && VERBOSE_LOGGING) {
    599                 Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList);
    600             }
    601         }
    602 
    603         if (mRawContactDelta == null) {
    604             // Selected contact is read-only, and there's no editable account.
    605             mIsReadOnly = true;
    606             mEditableAccountType = null;
    607         } else {
    608             mIsReadOnly = false;
    609 
    610             mEditableAccountType = mRawContactDelta.getRawContactAccountType(this);
    611 
    612             // Handle any incoming values that should be inserted
    613             final Bundle extras = getIntent().getExtras();
    614             if (extras != null && extras.size() > 0) {
    615                 // If there are any intent extras, add them as additional fields in the
    616                 // RawContactDelta.
    617                 RawContactModifier.parseExtras(this, mEditableAccountType, mRawContactDelta,
    618                         extras);
    619             }
    620         }
    621 
    622         bindEditor();
    623     }
    624 
    625     /**
    626      * Create an {@link RawContactDelta} for a raw_contact on the first editable account found, and add
    627      * to the list.  Also copy the structured name from an existing (read-only) raw_contact to the
    628      * new one, if any of the read-only contacts has a name.
    629      */
    630     private static RawContactDelta addEditableRawContact(Context context,
    631             RawContactDeltaList entityDeltaList) {
    632         // First, see if there's an editable account.
    633         final AccountTypeManager accounts = AccountTypeManager.getInstance(context);
    634         final List<AccountWithDataSet> editableAccounts = accounts.getAccounts(true);
    635         if (editableAccounts.size() == 0) {
    636             // No editable account type found.  The dialog will be read-only mode.
    637             return null;
    638         }
    639         final AccountWithDataSet editableAccount = editableAccounts.get(0);
    640         final AccountType accountType = accounts.getAccountType(
    641                 editableAccount.type, editableAccount.dataSet);
    642 
    643         // Create a new RawContactDelta for the new raw_contact.
    644         final RawContact rawContact = new RawContact();
    645         rawContact.setAccount(editableAccount);
    646 
    647         final RawContactDelta entityDelta = new RawContactDelta(ValuesDelta.fromAfter(
    648                 rawContact.getValues()));
    649 
    650         // Then, copy the structure name from an existing (read-only) raw_contact.
    651         for (RawContactDelta entity : entityDeltaList) {
    652             final ArrayList<ValuesDelta> readOnlyNames =
    653                     entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
    654             if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) {
    655                 final ValuesDelta readOnlyName = readOnlyNames.get(0);
    656                 final ValuesDelta newName = RawContactModifier.ensureKindExists(entityDelta,
    657                         accountType, StructuredName.CONTENT_ITEM_TYPE);
    658 
    659                 // Copy all the data fields.
    660                 newName.copyStructuredNameFieldsFrom(readOnlyName);
    661                 break;
    662             }
    663         }
    664 
    665         // Add the new RawContactDelta to the list.
    666         entityDeltaList.add(entityDelta);
    667 
    668         return entityDelta;
    669     }
    670 
    671     /**
    672      * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object.
    673      */
    674     private void bindEditor() {
    675         if (mEntityDeltaList == null) {
    676             throw new IllegalStateException();
    677         }
    678 
    679         // If no valid raw contact (to insert the data) was found, we won't have an editable
    680         // account type to use. In this case, display an error message and hide the "OK" button.
    681         if (mIsReadOnly) {
    682             mReadOnlyWarningView.setText(getString(R.string.contact_read_only));
    683             mReadOnlyWarningView.setVisibility(View.VISIBLE);
    684             mEditorContainerView.setVisibility(View.GONE);
    685             findViewById(R.id.btn_done).setVisibility(View.GONE);
    686             // Nothing more to be done, just show the UI
    687             showDialogContent();
    688             return;
    689         }
    690 
    691         // Otherwise display an editor that allows the user to add the data to this raw contact.
    692         for (DataKind kind : mEditableAccountType.getSortedDataKinds()) {
    693             // Skip kind that are not editable
    694             if (!kind.editable) continue;
    695             if (mMimetype.equals(kind.mimeType)) {
    696                 final ArrayList<ValuesDelta> deltas = mRawContactDelta.getMimeEntries(mMimetype);
    697                 if (deltas != null) {
    698                     for (ValuesDelta valuesDelta : deltas) {
    699                         // Skip entries that aren't visible
    700                         if (!valuesDelta.isVisible()) continue;
    701                         if (valuesDelta.isInsert()) {
    702                             inflateEditorView(kind, valuesDelta, mRawContactDelta);
    703                             return;
    704                         }
    705                     }
    706                 }
    707             }
    708         }
    709     }
    710 
    711     /**
    712      * Creates an EditorView for the given entry. This function must be used while constructing
    713      * the views corresponding to the the object-model. The resulting EditorView is also added
    714      * to the end of mEditors
    715      */
    716     private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta state) {
    717         final int layoutResId = EditorUiUtils.getLayoutResourceId(dataKind.mimeType);
    718         final View view = mInflater.inflate(layoutResId, mEditorContainerView,
    719                 false);
    720 
    721         if (view instanceof Editor) {
    722             Editor editor = (Editor) view;
    723             // Don't allow deletion of the field because there is only 1 detail in this editor.
    724             editor.setDeletable(false);
    725             editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator());
    726         }
    727 
    728         mEditorContainerView.addView(view);
    729     }
    730 
    731     /**
    732      * Set the display name to the correct TextView. Don't do this until it is
    733      * certain there is no need for a disambiguation field (otherwise the screen
    734      * will flicker because the name will be centered and then moved upwards).
    735      */
    736     private void setDisplayName() {
    737         mDisplayNameView.setText(mDisplayName);
    738     }
    739 
    740     /**
    741      * Set the TextView (for extra contact info) with the given value and make the
    742      * TextView visible.
    743      */
    744     private void setExtraInfoField(String value) {
    745         TextView extraTextView = (TextView) findViewById(R.id.extra_info);
    746         extraTextView.setVisibility(View.VISIBLE);
    747         extraTextView.setText(value);
    748     }
    749 
    750     private void setDefaultContactImage(String displayName, String lookupKey) {
    751         mPhotoView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
    752                 getResources(), false,
    753                 new DefaultImageRequest(displayName, lookupKey, false /* isCircular */)));
    754     }
    755 
    756     /**
    757      * Shows all the contents of the dialog to the user at one time. This should only be called
    758      * once all the queries have completed, otherwise the screen will flash as additional data
    759      * comes in.
    760      */
    761     private void showDialogContent() {
    762         mRootView.setVisibility(View.VISIBLE);
    763     }
    764 
    765     /**
    766      * Saves or creates the contact based on the mode, and if successful
    767      * finishes the activity.
    768      */
    769     private void doSaveAction() {
    770         final PersistTask task = new PersistTask(this, mAccountTypeManager);
    771         task.execute(mEntityDeltaList);
    772     }
    773 
    774     /**
    775      * Background task for persisting edited contact data, using the changes
    776      * defined by a set of {@link RawContactDelta}. This task starts
    777      * {@link EmptyService} to make sure the background thread can finish
    778      * persisting in cases where the system wants to reclaim our process.
    779      */
    780     private static class PersistTask extends AsyncTask<RawContactDeltaList, Void, Integer> {
    781         // In the future, use ContactSaver instead of WeakAsyncTask because of
    782         // the danger of the activity being null during a save action
    783         private static final int PERSIST_TRIES = 3;
    784 
    785         private static final int RESULT_UNCHANGED = 0;
    786         private static final int RESULT_SUCCESS = 1;
    787         private static final int RESULT_FAILURE = 2;
    788 
    789         private ConfirmAddDetailActivity activityTarget;
    790 
    791         private AccountTypeManager mAccountTypeManager;
    792 
    793         public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) {
    794             activityTarget = target;
    795             mAccountTypeManager = accountTypeManager;
    796         }
    797 
    798         @Override
    799         protected void onPreExecute() {
    800             sProgressDialog = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget,
    801                     null, activityTarget.getText(R.string.savingContact)));
    802 
    803             // Before starting this task, start an empty service to protect our
    804             // process from being reclaimed by the system.
    805             final Context context = activityTarget;
    806             context.startService(new Intent(context, EmptyService.class));
    807         }
    808 
    809         @Override
    810         protected Integer doInBackground(RawContactDeltaList... params) {
    811             final Context context = activityTarget;
    812             final ContentResolver resolver = context.getContentResolver();
    813 
    814             RawContactDeltaList state = params[0];
    815 
    816             if (state == null) {
    817                 return RESULT_FAILURE;
    818             }
    819 
    820             // Trim any empty fields, and RawContacts, before persisting
    821             RawContactModifier.trimEmpty(state, mAccountTypeManager);
    822 
    823             // Attempt to persist changes
    824             int tries = 0;
    825             Integer result = RESULT_FAILURE;
    826             while (tries++ < PERSIST_TRIES) {
    827                 try {
    828                     // Build operations and try applying
    829                     // Note: In case we've created a new raw_contact because the selected contact
    830                     // is read-only, buildDiff() will create aggregation exceptions to join
    831                     // the new one to the existing contact.
    832                     final ArrayList<ContentProviderOperation> diff = state.buildDiff();
    833                     ContentProviderResult[] results = null;
    834                     if (!diff.isEmpty()) {
    835                          results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
    836                     }
    837 
    838                     result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
    839                     break;
    840 
    841                 } catch (RemoteException e) {
    842                     // Something went wrong, bail without success
    843                     Log.e(TAG, "Problem persisting user edits", e);
    844                     break;
    845 
    846                 } catch (OperationApplicationException e) {
    847                     // Version consistency failed, bail without success
    848                     Log.e(TAG, "Version consistency failed", e);
    849                     break;
    850                 }
    851             }
    852 
    853             return result;
    854         }
    855 
    856         /** {@inheritDoc} */
    857         @Override
    858         protected void onPostExecute(Integer result) {
    859             final Context context = activityTarget;
    860 
    861             dismissProgressDialog();
    862 
    863             // Show a toast message based on the success or failure of the save action.
    864             if (result == RESULT_SUCCESS) {
    865                 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
    866             } else if (result == RESULT_FAILURE) {
    867                 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
    868             }
    869 
    870             // Stop the service that was protecting us
    871             context.stopService(new Intent(context, EmptyService.class));
    872             activityTarget.onSaveCompleted(result != RESULT_FAILURE);
    873         }
    874     }
    875 
    876     @Override
    877     protected void onStop() {
    878         super.onStop();
    879         // Dismiss the progress dialog here to prevent leaking the window on orientation change.
    880         dismissProgressDialog();
    881     }
    882 
    883     /**
    884      * Dismiss the progress dialog (check if it is null because it is a {@link WeakReference}).
    885      */
    886     private static void dismissProgressDialog() {
    887         ProgressDialog dialog = (sProgressDialog == null) ? null : sProgressDialog.get();
    888         if (dialog != null) {
    889             dialog.dismiss();
    890         }
    891         sProgressDialog = null;
    892     }
    893 
    894     /**
    895      * This method is intended to be executed after the background task for saving edited info has
    896      * finished. The method sets the activity result (and intent if applicable) and finishes the
    897      * activity.
    898      * @param success is true if the save task completed successfully, or false otherwise.
    899      */
    900     private void onSaveCompleted(boolean success) {
    901         if (success) {
    902             Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri);
    903             setResult(RESULT_OK, intent);
    904         } else {
    905             setResult(RESULT_CANCELED);
    906         }
    907         finish();
    908     }
    909 }
    910