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