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