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