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