/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.silentcircle.contacts.activities; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.AsyncQueryHandler; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.net.Uri.Builder; import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.silentcircle.contacts.model.RawContact; import com.silentcircle.contacts.model.RawContactDelta; import com.silentcircle.contacts.model.dataitem.DataKind; import com.silentcircle.contacts.utils.EmptyService; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Email; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Im; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Nickname; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Phone; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Photo; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.StructuredName; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.StructuredPostal; import com.silentcircle.silentcontacts.ScContactsContract.Data; import com.silentcircle.silentcontacts.ScContactsContract.RawContacts; import com.silentcircle.silentcontacts.ScContactsContract.RawContactsEntity; import com.silentcircle.contacts.editor.Editor; import com.silentcircle.contacts.editor.ViewIdGenerator; import com.silentcircle.contacts.model.AccountTypeManager; import com.silentcircle.contacts.model.RawContactDeltaList; import com.silentcircle.contacts.model.RawContactModifier; import com.silentcircle.contacts.model.account.AccountType; import com.silentcircle.contacts.utils.DialogManager; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; /** * This is a dialog-themed activity for confirming the addition of a detail to an existing contact * (once the user has selected this contact from a list of all contacts). The incoming intent * must have an extra with max 1 phone or email specified, using * {@link com.silentcircle.silentcontacts.ScContactsContract.Intents.Insert#PHONE} with type * {@link com.silentcircle.silentcontacts.ScContactsContract.Intents.Insert#PHONE_TYPE} or * {@link com.silentcircle.silentcontacts.ScContactsContract.Intents.Insert#EMAIL} with type * {@link com.silentcircle.silentcontacts.ScContactsContract.Intents.Insert#EMAIL_TYPE} intent keys. * * If the selected contact doesn't contain editable raw_contacts, it'll create a new raw_contact * on the first editable account found, and the data will be added to this raw_contact. The newly * created raw_contact will be joined with the selected contact with aggregation-exceptions. * * TODO: Don't open this activity if there's no editable accounts. * If there's no editable accounts on the system, we'll set {@link #mIsReadOnly} and the dialog * just says "contact is not editable". It's slightly misleading because this really means * "there's no editable accounts", but in this case we shouldn't show the contact picker in the * first place. * Note when there's no accounts, it *is* okay to show the picker / dialog, because the local-only * contacts are writable. */ public class ConfirmAddDetailActivity extends Activity implements DialogManager.DialogShowingViewActivity { private static final String TAG = "ConfirmAdd"; // The class name is too long to be a tag. private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); private LayoutInflater mInflater; private View mRootView; private TextView mDisplayNameView; private TextView mReadOnlyWarningView; private ImageView mPhotoView; private ViewGroup mEditorContainerView; private static WeakReference<ProgressDialog> sProgressDialog; private AccountTypeManager mAccountTypeManager; private ContentResolver mContentResolver; private AccountType mEditableAccountType; private Uri mContactUri; private long mContactId; private String mDisplayName; private boolean mIsReadOnly; private QueryHandler mQueryHandler; /** {@link RawContactDeltaList} for the entire selected contact. */ private RawContactDeltaList mEntityDeltaList; /** {@link RawContactDeltaList} for the editable account */ private RawContactDelta mRawContactDelta; private String mMimetype = Phone.CONTENT_ITEM_TYPE; /** * DialogManager may be needed if the user wants to apply a "custom" label to the contact detail */ private final DialogManager mDialogManager = new DialogManager(this); /** * PhotoQuery contains the projection used for retrieving the name and photo * ID of a contact. */ private interface ContactQuery { final String[] COLUMNS = new String[] { RawContacts._ID, RawContacts.PHOTO_ID, RawContacts.DISPLAY_NAME, }; final int _ID = 0; final int PHOTO_ID = 1; final int DISPLAY_NAME = 2; } /** * PhotoQuery contains the projection used for retrieving the raw bytes of * the contact photo. */ private interface PhotoQuery { final String[] COLUMNS = new String[] { Photo.PHOTO }; final int PHOTO = 0; } /** * ExtraInfoQuery contains the projection used for retrieving the extra info * on a contact (only needed if someone else exists with the same name as * this contact). */ private interface ExtraInfoQuery { final String[] COLUMNS = new String[] { RawContacts._ID, Data.MIMETYPE, Data.DATA1, }; final int CONTACT_ID = 0; final int MIMETYPE = 1; final int DATA1 = 2; } /** * List of mimetypes to use in order of priority to display for a contact in * a disambiguation case. For example, if the contact does not have a * nickname, use the email field, and etc. */ private static final String[] MIME_TYPE_PRIORITY_LIST = new String[] { Nickname.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE }; private static final int TOKEN_CONTACT_INFO = 0; private static final int TOKEN_PHOTO_QUERY = 1; private static final int TOKEN_DISAMBIGUATION_QUERY = 2; private static final int TOKEN_EXTRA_INFO_QUERY = 3; private final OnClickListener mDetailsButtonClickListener = new OnClickListener() { @Override public void onClick(View v) { if (mIsReadOnly) { onSaveCompleted(true); } else { doSaveAction(); } } }; private final OnClickListener mDoneButtonClickListener = new OnClickListener() { @Override public void onClick(View v) { doSaveAction(); } }; private final OnClickListener mCancelButtonClickListener = new OnClickListener() { @Override public void onClick(View v) { setResult(RESULT_CANCELED); finish(); } }; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); mContentResolver = getContentResolver(); final Intent intent = getIntent(); mContactUri = intent.getData(); if (mContactUri == null) { setResult(RESULT_CANCELED); finish(); } Bundle extras = intent.getExtras(); if (extras != null) { if (extras.containsKey(ScContactsContract.Intents.Insert.PHONE)) { mMimetype = Phone.CONTENT_ITEM_TYPE; } else if (extras.containsKey(ScContactsContract.Intents.Insert.EMAIL)) { mMimetype = Email.CONTENT_ITEM_TYPE; } else { throw new IllegalStateException("Error: No valid mimetype found in intent extras"); } } mAccountTypeManager = AccountTypeManager.getInstance(this); setContentView(R.layout.confirm_add_detail_activity); mRootView = findViewById(R.id.root_view); mReadOnlyWarningView = (TextView) findViewById(R.id.read_only_warning); // Setup "header" (containing contact info) to save the detail and then go to the editor findViewById(R.id.open_details_push_layer).setOnClickListener(mDetailsButtonClickListener); // Setup "done" button to save the detail to the contact and exit. findViewById(R.id.btn_done).setOnClickListener(mDoneButtonClickListener); // Setup "cancel" button to return to previous activity. findViewById(R.id.btn_cancel).setOnClickListener(mCancelButtonClickListener); // Retrieve references to all the Views in the dialog activity. mDisplayNameView = (TextView) findViewById(R.id.name); mPhotoView = (ImageView) findViewById(R.id.photo); mEditorContainerView = (ViewGroup) findViewById(R.id.editor_container); resetAsyncQueryHandler(); startContactQuery(mContactUri); new QueryEntitiesTask(this).execute(intent); } @Override public DialogManager getDialogManager() { return mDialogManager; } @Override protected Dialog onCreateDialog(int id, Bundle args) { if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args); // Nobody knows about the Dialog Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args); return null; } /** * Reset the query handler by creating a new QueryHandler instance. */ private void resetAsyncQueryHandler() { // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really // need the old async queries to be cancelled, let's do it the hard way. mQueryHandler = new QueryHandler(mContentResolver); } /** * Internal method to query contact by Uri. * * @param contactUri the contact uri */ private void startContactQuery(Uri contactUri) { mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS, null, null, null); } /** * Internal method to query contact photo by photo id and uri. * * @param photoId the photo id. * @param lookupKey the lookup uri. */ private void startPhotoQuery(long photoId, Uri lookupKey) { mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey, ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS, null, null, null); } /** * Internal method to query for contacts with a given display name. * * @param contactDisplayName the display name to look for. */ private void startDisambiguationQuery(String contactDisplayName) { // Apply a limit of 1 result to the query because we only need to // determine whether or not at least one other contact has the same // name. We don't need to find ALL other contacts with the same name. final Builder builder = RawContacts.CONTENT_URI.buildUpon(); builder.appendQueryParameter("limit", String.valueOf(1)); final Uri uri = builder.build(); final String displayNameSelection; final String[] selectionArgs; if (TextUtils.isEmpty(contactDisplayName)) { displayNameSelection = RawContacts.DISPLAY_NAME_PRIMARY + " IS NULL"; selectionArgs = new String[] { String.valueOf(mContactId) }; } else { displayNameSelection = RawContacts.DISPLAY_NAME_PRIMARY + " = ?"; selectionArgs = new String[] { contactDisplayName, String.valueOf(mContactId) }; } mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri, new String[] { RawContacts._ID }, // unused projection but a valid one was needed displayNameSelection + " AND " + RawContacts.PHOTO_ID + " IS NULL AND " + RawContacts._ID + " <> ?", selectionArgs, null); } /** * Internal method to query for extra data fields for this contact. */ private void startExtraInfoQuery() { mQueryHandler.startQuery(TOKEN_EXTRA_INFO_QUERY, null, Data.CONTENT_URI, ExtraInfoQuery.COLUMNS, RawContacts._ID + " = ?", new String[] { String.valueOf(mContactId) }, null); } private static class QueryEntitiesTask extends AsyncTask<Intent, Void, RawContactDeltaList> { private ConfirmAddDetailActivity activityTarget; private String mSelection; public QueryEntitiesTask(ConfirmAddDetailActivity target) { activityTarget = target; } @Override protected RawContactDeltaList doInBackground(Intent... params) { final Intent intent = params[0]; final ContentResolver resolver = activityTarget.getContentResolver(); // Handle both legacy and new authorities final Uri data = intent.getData(); final String mimeType = intent.resolveType(resolver); mSelection = "0"; String selectionArg = null; if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) { final long rawContactId = ContentUris.parseId(data); selectionArg = String.valueOf(rawContactId); mSelection = RawContacts._ID + "=?"; } // Note that this query does not need to concern itself with whether the contact is // the user's profile, since the profile does not show up in the picker. RawContactDeltaList rdl = RawContactDeltaList.fromQuery(RawContactsEntity.CONTENT_URI, activityTarget.getContentResolver(), mSelection, new String[] { selectionArg }, null); return rdl; } @Override protected void onPostExecute(RawContactDeltaList entityList) { if (activityTarget.isFinishing()) { return; } if ((entityList == null) || (entityList.size() == 0)) { Log.e(TAG, "Contact not found."); activityTarget.finish(); return; } activityTarget.setEntityDeltaList(entityList); } } private class QueryHandler extends AsyncQueryHandler { public QueryHandler(ContentResolver cr) { super(cr); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { try { if (this != mQueryHandler) { Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!"); return; } if (ConfirmAddDetailActivity.this.isFinishing()) { return; } switch (token) { case TOKEN_PHOTO_QUERY: { // Set the photo Bitmap photoBitmap = null; if (cursor != null && cursor.moveToFirst() && !cursor.isNull(PhotoQuery.PHOTO)) { byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO); photoBitmap = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null); } if (photoBitmap != null) { mPhotoView.setImageBitmap(photoBitmap); } break; } case TOKEN_CONTACT_INFO: { // Set the contact's name if (cursor != null && cursor.moveToFirst()) { // Get the cursor values mDisplayName = cursor.getString(ContactQuery.DISPLAY_NAME); final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); // If there is no photo ID, then do a disambiguation // query because other contacts could have the same // name as this contact. if (photoId == 0) { mContactId = cursor.getLong(ContactQuery._ID); startDisambiguationQuery(mDisplayName); } else { // Otherwise do the photo query. Uri lookupUri = RawContacts.getLookupUri(mContactId); startPhotoQuery(photoId, lookupUri); // Display the name because there is no // disambiguation query. setDisplayName(); showDialogContent(); } } break; } case TOKEN_DISAMBIGUATION_QUERY: { // If a cursor was returned with more than 0 results, // then at least one other contact exists with the same // name as this contact. Extra info on this contact must // be displayed to disambiguate the contact, so retrieve // those additional fields. Otherwise, no other contacts // with this name exists, so do nothing further. if (cursor != null && cursor.getCount() > 0) { startExtraInfoQuery(); } else { // If there are no other contacts with this name, // then display the name. setDisplayName(); showDialogContent(); } break; } case TOKEN_EXTRA_INFO_QUERY: { // This case should only occur if there are one or more // other contacts with the same contact name. if (cursor != null && cursor.moveToFirst()) { HashMap<String, String> hashMapCursorData = new HashMap<String, String>(); // Convert the cursor data into a hashmap of // (mimetype, data value) pairs. If a contact has // multiple values with the same mimetype, it's fine // to override that hashmap entry because we only // need one value of that type. while (!cursor.isAfterLast()) { final String mimeType = cursor.getString(ExtraInfoQuery.MIMETYPE); if (!TextUtils.isEmpty(mimeType)) { String value = cursor.getString(ExtraInfoQuery.DATA1); if (!TextUtils.isEmpty(value)) { // As a special case, phone numbers // should be formatted in a specific way. if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { value = PhoneNumberUtils.formatNumber(value); } hashMapCursorData.put(mimeType, value); } } cursor.moveToNext(); } // Find the first non-empty field according to the // mimetype priority list and display this under the // contact's display name to disambiguate the contact. for (String mimeType : MIME_TYPE_PRIORITY_LIST) { if (hashMapCursorData.containsKey(mimeType)) { setDisplayName(); setExtraInfoField(hashMapCursorData.get(mimeType)); break; } } showDialogContent(); } break; } } } finally { if (cursor != null) { cursor.close(); } } } } private void setEntityDeltaList(RawContactDeltaList entityList) { if (entityList == null) { throw new IllegalStateException(); } if (VERBOSE_LOGGING) { Log.v(TAG, "setEntityDeltaList: " + entityList); } mEntityDeltaList = entityList; // Find the editable raw_contact. mRawContactDelta = mEntityDeltaList.getFirstWritableRawContact(this); // If no editable raw_contacts are found, create one. if (mRawContactDelta == null) { mRawContactDelta = addEditableRawContact(this, mEntityDeltaList); if ((mRawContactDelta != null) && VERBOSE_LOGGING) { Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList); } } if (mRawContactDelta == null) { // Selected contact is read-only, and there's no editable account. mIsReadOnly = true; mEditableAccountType = null; } else { mIsReadOnly = false; mEditableAccountType = mRawContactDelta.getRawContactAccountType(this); // Handle any incoming values that should be inserted final Bundle extras = getIntent().getExtras(); if (extras != null && extras.size() > 0) { // If there are any intent extras, add them as additional fields in the // RawContactDelta. RawContactModifier.parseExtras(this, mEditableAccountType, mRawContactDelta, extras); } } bindEditor(); } /** * Create an {@link RawContactDelta} for a raw_contact on the first editable account found, and add * to the list. Also copy the structured name from an existing (read-only) raw_contact to the * new one, if any of the read-only contacts has a name. */ private static RawContactDelta addEditableRawContact(Context context, RawContactDeltaList entityDeltaList) { // First, see if there's an editable account. final AccountTypeManager accounts = AccountTypeManager.getInstance(context); // final List<AccountWithDataSet> editableAccounts = accounts.getAccounts(true); // if (editableAccounts.size() == 0) { // // No editable account type found. The dialog will be read-only mode. // return null; // } // final AccountWithDataSet editableAccount = editableAccounts.get(0); final AccountType accountType = accounts.getAccountType(); // Create a new RawContactDelta for the new raw_contact. final RawContact rawContact = new RawContact(context); rawContact.setAccountToLocal(); final RawContactDelta entityDelta = new RawContactDelta(RawContactDelta.ValuesDelta.fromAfter( rawContact.getValues())); // Then, copy the structure name from an existing (read-only) raw_contact. for (RawContactDelta entity : entityDeltaList) { final ArrayList<RawContactDelta.ValuesDelta> readOnlyNames = entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE); if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) { final RawContactDelta.ValuesDelta readOnlyName = readOnlyNames.get(0); final RawContactDelta.ValuesDelta newName = RawContactModifier.ensureKindExists(entityDelta, accountType, StructuredName.CONTENT_ITEM_TYPE); // Copy all the data fields. newName.copyStructuredNameFieldsFrom(readOnlyName); break; } } // Add the new RawContactDelta to the list. entityDeltaList.add(entityDelta); return entityDelta; } /** * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object. */ private void bindEditor() { if (mEntityDeltaList == null) { throw new IllegalStateException(); } // If no valid raw contact (to insert the data) was found, we won't have an editable // account type to use. In this case, display an error message and hide the "OK" button. if (mIsReadOnly) { mReadOnlyWarningView.setText(getString(R.string.contact_read_only)); mReadOnlyWarningView.setVisibility(View.VISIBLE); mEditorContainerView.setVisibility(View.GONE); findViewById(R.id.btn_done).setVisibility(View.GONE); // Nothing more to be done, just show the UI showDialogContent(); return; } // Otherwise display an editor that allows the user to add the data to this raw contact. for (DataKind kind : mEditableAccountType.getSortedDataKinds()) { // Skip kind that are not editable if (!kind.editable) continue; if (mMimetype.equals(kind.mimeType)) { for (RawContactDelta.ValuesDelta valuesDelta : mRawContactDelta.getMimeEntries(mMimetype)) { // Skip entries that aren't visible if (!valuesDelta.isVisible()) continue; if (valuesDelta.isInsert()) { inflateEditorView(kind, valuesDelta, mRawContactDelta); return; } } } } } /** * Creates an EditorView for the given entry. This function must be used while constructing * the views corresponding to the the object-model. The resulting EditorView is also added * to the end of mEditors */ private void inflateEditorView(DataKind dataKind, RawContactDelta.ValuesDelta valuesDelta, RawContactDelta state) { final View view = mInflater.inflate(dataKind.editorLayoutResourceId, mEditorContainerView, false); if (view instanceof Editor) { Editor editor = (Editor) view; // Don't allow deletion of the field because there is only 1 detail in this editor. editor.setDeletable(false); editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator()); } mEditorContainerView.addView(view); } /** * Set the display name to the correct TextView. Don't do this until it is * certain there is no need for a disambiguation field (otherwise the screen * will flicker because the name will be centered and then moved upwards). */ private void setDisplayName() { mDisplayNameView.setText(mDisplayName); } /** * Set the TextView (for extra contact info) with the given value and make the * TextView visible. */ private void setExtraInfoField(String value) { TextView extraTextView = (TextView) findViewById(R.id.extra_info); extraTextView.setVisibility(View.VISIBLE); extraTextView.setText(value); } /** * Shows all the contents of the dialog to the user at one time. This should only be called * once all the queries have completed, otherwise the screen will flash as additional data * comes in. */ private void showDialogContent() { mRootView.setVisibility(View.VISIBLE); } /** * Saves or creates the contact based on the mode, and if successful * finishes the activity. */ private void doSaveAction() { final PersistTask task = new PersistTask(this, mAccountTypeManager); task.execute(mEntityDeltaList); } /** * Background task for persisting edited contact data, using the changes * defined by a set of {@link RawContactDelta}. This task starts * {@link com.silentcircle.contacts.utils.EmptyService} to make sure the background thread can finish * persisting in cases where the system wants to reclaim our process. */ private static class PersistTask extends AsyncTask<RawContactDeltaList, Void, Integer> { // In the future, use ContactSaver instead of WeakAsyncTask because of // the danger of the activity being null during a save action private static final int PERSIST_TRIES = 3; private static final int RESULT_UNCHANGED = 0; private static final int RESULT_SUCCESS = 1; private static final int RESULT_FAILURE = 2; private ConfirmAddDetailActivity activityTarget; private AccountTypeManager mAccountTypeManager; public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) { activityTarget = target; mAccountTypeManager = accountTypeManager; } @Override protected void onPreExecute() { sProgressDialog = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget, null, activityTarget.getText(R.string.savingContact))); // Before starting this task, start an empty service to protect our // process from being reclaimed by the system. final Context context = activityTarget; context.startService(new Intent(context, EmptyService.class)); } @Override protected Integer doInBackground(RawContactDeltaList... params) { final Context context = activityTarget; final ContentResolver resolver = context.getContentResolver(); RawContactDeltaList state = params[0]; if (state == null) { return RESULT_FAILURE; } // Trim any empty fields, and RawContacts, before persisting RawContactModifier.trimEmpty(state, mAccountTypeManager); // Attempt to persist changes int tries = 0; Integer result = RESULT_FAILURE; while (tries++ < PERSIST_TRIES) { try { // Build operations and try applying // Note: In case we've created a new raw_contact because the selected contact // is read-only, buildDiff() will create aggregation exceptions to join // the new one to the existing contact. final ArrayList<ContentProviderOperation> diff = state.buildDiff(); ContentProviderResult[] results = null; if (!diff.isEmpty()) { results = resolver.applyBatch(ScContactsContract.AUTHORITY, diff); } result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED; break; } catch (RemoteException e) { // Something went wrong, bail without success Log.e(TAG, "Problem persisting user edits", e); break; } catch (OperationApplicationException e) { // Version consistency failed, bail without success Log.e(TAG, "Version consistency failed", e); break; } } return result; } /** {@inheritDoc} */ @Override protected void onPostExecute(Integer result) { final Context context = activityTarget; dismissProgressDialog(); // Show a toast message based on the success or failure of the save action. if (result == RESULT_SUCCESS) { Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); } else if (result == RESULT_FAILURE) { Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); } // Stop the service that was protecting us context.stopService(new Intent(context, EmptyService.class)); activityTarget.onSaveCompleted(result != RESULT_FAILURE); } } @Override protected void onStop() { super.onStop(); // Dismiss the progress dialog here to prevent leaking the window on orientation change. dismissProgressDialog(); } /** * Dismiss the progress dialog (check if it is null because it is a {@link WeakReference}). */ private static void dismissProgressDialog() { ProgressDialog dialog = (sProgressDialog == null) ? null : sProgressDialog.get(); if (dialog != null) { dialog.dismiss(); } sProgressDialog = null; } /** * This method is intended to be executed after the background task for saving edited info has * finished. The method sets the activity result (and intent if applicable) and finishes the * activity. * @param success is true if the save task completed successfully, or false otherwise. */ private void onSaveCompleted(boolean success) { if (success) { Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri); setResult(RESULT_OK, intent); } else { setResult(RESULT_CANCELED); } finish(); } }