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
17package com.android.contacts.activities;
18
19import android.app.Activity;
20import android.app.Dialog;
21import android.app.ProgressDialog;
22import android.content.AsyncQueryHandler;
23import android.content.ContentProviderOperation;
24import android.content.ContentProviderResult;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.Context;
28import android.content.Intent;
29import android.content.OperationApplicationException;
30import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.graphics.BitmapFactory;
33import android.net.Uri;
34import android.net.Uri.Builder;
35import android.os.AsyncTask;
36import android.os.Bundle;
37import android.os.RemoteException;
38import android.provider.ContactsContract;
39import android.provider.ContactsContract.CommonDataKinds.Email;
40import android.provider.ContactsContract.CommonDataKinds.Im;
41import android.provider.ContactsContract.CommonDataKinds.Nickname;
42import android.provider.ContactsContract.CommonDataKinds.Phone;
43import android.provider.ContactsContract.CommonDataKinds.Photo;
44import android.provider.ContactsContract.CommonDataKinds.StructuredName;
45import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
46import android.provider.ContactsContract.Contacts;
47import android.provider.ContactsContract.Data;
48import android.provider.ContactsContract.RawContacts;
49import android.provider.ContactsContract.RawContactsEntity;
50import android.telephony.PhoneNumberUtils;
51import android.text.TextUtils;
52import android.util.Log;
53import android.view.LayoutInflater;
54import android.view.View;
55import android.view.View.OnClickListener;
56import android.view.ViewGroup;
57import android.widget.ImageView;
58import android.widget.TextView;
59import android.widget.Toast;
60
61import com.android.contacts.R;
62import com.android.contacts.editor.Editor;
63import com.android.contacts.editor.ViewIdGenerator;
64import com.android.contacts.model.AccountTypeManager;
65import com.android.contacts.model.RawContact;
66import com.android.contacts.model.RawContactDelta;
67import com.android.contacts.model.RawContactDelta.ValuesDelta;
68import com.android.contacts.model.RawContactDeltaList;
69import com.android.contacts.model.RawContactModifier;
70import com.android.contacts.model.account.AccountType;
71import com.android.contacts.model.account.AccountWithDataSet;
72import com.android.contacts.model.dataitem.DataKind;
73import com.android.contacts.util.DialogManager;
74import com.android.contacts.util.EmptyService;
75
76import java.lang.ref.WeakReference;
77import java.util.ArrayList;
78import java.util.HashMap;
79import 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 */
102public 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 RawContactDeltaList} for the entire selected contact. */
128    private RawContactDeltaList mEntityDeltaList;
129
130    /** {@link RawContactDeltaList} for the editable account */
131    private RawContactDelta mRawContactDelta;
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, RawContactDeltaList> {
361
362        private ConfirmAddDetailActivity activityTarget;
363        private String mSelection;
364
365        public QueryEntitiesTask(ConfirmAddDetailActivity target) {
366            activityTarget = target;
367        }
368
369        @Override
370        protected RawContactDeltaList 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 RawContactDeltaList.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(RawContactDeltaList 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(RawContactDeltaList 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        mRawContactDelta = mEntityDeltaList.getFirstWritableRawContact(this);
582
583        // If no editable raw_contacts are found, create one.
584        if (mRawContactDelta == null) {
585            mRawContactDelta = addEditableRawContact(this, mEntityDeltaList);
586
587            if ((mRawContactDelta != null) && VERBOSE_LOGGING) {
588                Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList);
589            }
590        }
591
592        if (mRawContactDelta == 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 = mRawContactDelta.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
605                // RawContactDelta.
606                RawContactModifier.parseExtras(this, mEditableAccountType, mRawContactDelta,
607                        extras);
608            }
609        }
610
611        bindEditor();
612    }
613
614    /**
615     * Create an {@link RawContactDelta} for a raw_contact on the first editable account found, and add
616     * to the list.  Also copy the structured name from an existing (read-only) raw_contact to the
617     * new one, if any of the read-only contacts has a name.
618     */
619    private static RawContactDelta addEditableRawContact(Context context,
620            RawContactDeltaList entityDeltaList) {
621        // First, see if there's an editable account.
622        final AccountTypeManager accounts = AccountTypeManager.getInstance(context);
623        final List<AccountWithDataSet> editableAccounts = accounts.getAccounts(true);
624        if (editableAccounts.size() == 0) {
625            // No editable account type found.  The dialog will be read-only mode.
626            return null;
627        }
628        final AccountWithDataSet editableAccount = editableAccounts.get(0);
629        final AccountType accountType = accounts.getAccountType(
630                editableAccount.type, editableAccount.dataSet);
631
632        // Create a new RawContactDelta for the new raw_contact.
633        final RawContact rawContact = new RawContact(context);
634        rawContact.setAccount(editableAccount);
635
636        final RawContactDelta entityDelta = new RawContactDelta(ValuesDelta.fromAfter(
637                rawContact.getValues()));
638
639        // Then, copy the structure name from an existing (read-only) raw_contact.
640        for (RawContactDelta entity : entityDeltaList) {
641            final ArrayList<ValuesDelta> readOnlyNames =
642                    entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
643            if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) {
644                final ValuesDelta readOnlyName = readOnlyNames.get(0);
645                final ValuesDelta newName = RawContactModifier.ensureKindExists(entityDelta,
646                        accountType, StructuredName.CONTENT_ITEM_TYPE);
647
648                // Copy all the data fields.
649                newName.copyStructuredNameFieldsFrom(readOnlyName);
650                break;
651            }
652        }
653
654        // Add the new RawContactDelta to the list.
655        entityDeltaList.add(entityDelta);
656
657        return entityDelta;
658    }
659
660    /**
661     * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object.
662     */
663    private void bindEditor() {
664        if (mEntityDeltaList == null) {
665            throw new IllegalStateException();
666        }
667
668        // If no valid raw contact (to insert the data) was found, we won't have an editable
669        // account type to use. In this case, display an error message and hide the "OK" button.
670        if (mIsReadOnly) {
671            mReadOnlyWarningView.setText(getString(R.string.contact_read_only));
672            mReadOnlyWarningView.setVisibility(View.VISIBLE);
673            mEditorContainerView.setVisibility(View.GONE);
674            findViewById(R.id.btn_done).setVisibility(View.GONE);
675            // Nothing more to be done, just show the UI
676            showDialogContent();
677            return;
678        }
679
680        // Otherwise display an editor that allows the user to add the data to this raw contact.
681        for (DataKind kind : mEditableAccountType.getSortedDataKinds()) {
682            // Skip kind that are not editable
683            if (!kind.editable) continue;
684            if (mMimetype.equals(kind.mimeType)) {
685                for (ValuesDelta valuesDelta : mRawContactDelta.getMimeEntries(mMimetype)) {
686                    // Skip entries that aren't visible
687                    if (!valuesDelta.isVisible()) continue;
688                    if (valuesDelta.isInsert()) {
689                        inflateEditorView(kind, valuesDelta, mRawContactDelta);
690                        return;
691                    }
692                }
693            }
694        }
695    }
696
697    /**
698     * Creates an EditorView for the given entry. This function must be used while constructing
699     * the views corresponding to the the object-model. The resulting EditorView is also added
700     * to the end of mEditors
701     */
702    private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta state) {
703        final View view = mInflater.inflate(dataKind.editorLayoutResourceId, mEditorContainerView,
704                false);
705
706        if (view instanceof Editor) {
707            Editor editor = (Editor) view;
708            // Don't allow deletion of the field because there is only 1 detail in this editor.
709            editor.setDeletable(false);
710            editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator());
711        }
712
713        mEditorContainerView.addView(view);
714    }
715
716    /**
717     * Set the display name to the correct TextView. Don't do this until it is
718     * certain there is no need for a disambiguation field (otherwise the screen
719     * will flicker because the name will be centered and then moved upwards).
720     */
721    private void setDisplayName() {
722        mDisplayNameView.setText(mDisplayName);
723    }
724
725    /**
726     * Set the TextView (for extra contact info) with the given value and make the
727     * TextView visible.
728     */
729    private void setExtraInfoField(String value) {
730        TextView extraTextView = (TextView) findViewById(R.id.extra_info);
731        extraTextView.setVisibility(View.VISIBLE);
732        extraTextView.setText(value);
733    }
734
735    /**
736     * Shows all the contents of the dialog to the user at one time. This should only be called
737     * once all the queries have completed, otherwise the screen will flash as additional data
738     * comes in.
739     */
740    private void showDialogContent() {
741        mRootView.setVisibility(View.VISIBLE);
742    }
743
744    /**
745     * Saves or creates the contact based on the mode, and if successful
746     * finishes the activity.
747     */
748    private void doSaveAction() {
749        final PersistTask task = new PersistTask(this, mAccountTypeManager);
750        task.execute(mEntityDeltaList);
751    }
752
753    /**
754     * Background task for persisting edited contact data, using the changes
755     * defined by a set of {@link RawContactDelta}. This task starts
756     * {@link EmptyService} to make sure the background thread can finish
757     * persisting in cases where the system wants to reclaim our process.
758     */
759    private static class PersistTask extends AsyncTask<RawContactDeltaList, Void, Integer> {
760        // In the future, use ContactSaver instead of WeakAsyncTask because of
761        // the danger of the activity being null during a save action
762        private static final int PERSIST_TRIES = 3;
763
764        private static final int RESULT_UNCHANGED = 0;
765        private static final int RESULT_SUCCESS = 1;
766        private static final int RESULT_FAILURE = 2;
767
768        private ConfirmAddDetailActivity activityTarget;
769
770        private AccountTypeManager mAccountTypeManager;
771
772        public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) {
773            activityTarget = target;
774            mAccountTypeManager = accountTypeManager;
775        }
776
777        @Override
778        protected void onPreExecute() {
779            sProgressDialog = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget,
780                    null, activityTarget.getText(R.string.savingContact)));
781
782            // Before starting this task, start an empty service to protect our
783            // process from being reclaimed by the system.
784            final Context context = activityTarget;
785            context.startService(new Intent(context, EmptyService.class));
786        }
787
788        @Override
789        protected Integer doInBackground(RawContactDeltaList... params) {
790            final Context context = activityTarget;
791            final ContentResolver resolver = context.getContentResolver();
792
793            RawContactDeltaList state = params[0];
794
795            if (state == null) {
796                return RESULT_FAILURE;
797            }
798
799            // Trim any empty fields, and RawContacts, before persisting
800            RawContactModifier.trimEmpty(state, mAccountTypeManager);
801
802            // Attempt to persist changes
803            int tries = 0;
804            Integer result = RESULT_FAILURE;
805            while (tries++ < PERSIST_TRIES) {
806                try {
807                    // Build operations and try applying
808                    // Note: In case we've created a new raw_contact because the selected contact
809                    // is read-only, buildDiff() will create aggregation exceptions to join
810                    // the new one to the existing contact.
811                    final ArrayList<ContentProviderOperation> diff = state.buildDiff();
812                    ContentProviderResult[] results = null;
813                    if (!diff.isEmpty()) {
814                         results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
815                    }
816
817                    result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
818                    break;
819
820                } catch (RemoteException e) {
821                    // Something went wrong, bail without success
822                    Log.e(TAG, "Problem persisting user edits", e);
823                    break;
824
825                } catch (OperationApplicationException e) {
826                    // Version consistency failed, bail without success
827                    Log.e(TAG, "Version consistency failed", e);
828                    break;
829                }
830            }
831
832            return result;
833        }
834
835        /** {@inheritDoc} */
836        @Override
837        protected void onPostExecute(Integer result) {
838            final Context context = activityTarget;
839
840            dismissProgressDialog();
841
842            // Show a toast message based on the success or failure of the save action.
843            if (result == RESULT_SUCCESS) {
844                Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
845            } else if (result == RESULT_FAILURE) {
846                Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
847            }
848
849            // Stop the service that was protecting us
850            context.stopService(new Intent(context, EmptyService.class));
851            activityTarget.onSaveCompleted(result != RESULT_FAILURE);
852        }
853    }
854
855    @Override
856    protected void onStop() {
857        super.onStop();
858        // Dismiss the progress dialog here to prevent leaking the window on orientation change.
859        dismissProgressDialog();
860    }
861
862    /**
863     * Dismiss the progress dialog (check if it is null because it is a {@link WeakReference}).
864     */
865    private static void dismissProgressDialog() {
866        ProgressDialog dialog = (sProgressDialog == null) ? null : sProgressDialog.get();
867        if (dialog != null) {
868            dialog.dismiss();
869        }
870        sProgressDialog = null;
871    }
872
873    /**
874     * This method is intended to be executed after the background task for saving edited info has
875     * finished. The method sets the activity result (and intent if applicable) and finishes the
876     * activity.
877     * @param success is true if the save task completed successfully, or false otherwise.
878     */
879    private void onSaveCompleted(boolean success) {
880        if (success) {
881            Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri);
882            setResult(RESULT_OK, intent);
883        } else {
884            setResult(RESULT_CANCELED);
885        }
886        finish();
887    }
888}
889