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 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 }