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