blob: c8adf95a90ff802538eb9f19e64161887feb7dae [file] [log] [blame]
/*
* 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.android.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.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
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.android.contacts.R;
import com.android.contacts.editor.Editor;
import com.android.contacts.editor.ViewIdGenerator;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.RawContact;
import com.android.contacts.model.RawContactDelta;
import com.android.contacts.model.RawContactDelta.ValuesDelta;
import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.model.RawContactModifier;
import com.android.contacts.model.account.AccountType;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.model.dataitem.DataKind;
import com.android.contacts.util.DialogManager;
import com.android.contacts.util.EmptyService;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* 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 android.provider.ContactsContract.Intents.Insert#PHONE} with type
* {@link android.provider.ContactsContract.Intents.Insert#PHONE_TYPE} or
* {@link android.provider.ContactsContract.Intents.Insert#EMAIL} with type
* {@link android.provider.ContactsContract.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[] {
Contacts._ID,
Contacts.LOOKUP_KEY,
Contacts.PHOTO_ID,
Contacts.DISPLAY_NAME,
};
final int _ID = 0;
final int LOOKUP_KEY = 1;
final int PHOTO_ID = 2;
final int DISPLAY_NAME = 3;
}
/**
* 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.CONTACT_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(ContactsContract.Intents.Insert.PHONE)) {
mMimetype = Phone.CONTENT_ITEM_TYPE;
} else if (extras.containsKey(ContactsContract.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 = Contacts.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 = Contacts.DISPLAY_NAME_PRIMARY + " IS NULL";
selectionArgs = new String[] { String.valueOf(mContactId) };
} else {
displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " = ?";
selectionArgs = new String[] { contactDisplayName, String.valueOf(mContactId) };
}
mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri,
new String[] { Contacts._ID } /* unused projection but a valid one was needed */,
displayNameSelection + " AND " + Contacts.PHOTO_ID + " IS NULL AND "
+ Contacts._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.CONTACT_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 authority = data.getAuthority();
final String mimeType = intent.resolveType(resolver);
mSelection = "0";
String selectionArg = null;
if (ContactsContract.AUTHORITY.equals(authority)) {
if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Handle selected aggregate
final long contactId = ContentUris.parseId(data);
selectionArg = String.valueOf(contactId);
mSelection = RawContacts.CONTACT_ID + "=?";
} else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
final long rawContactId = ContentUris.parseId(data);
final long contactId = queryForContactId(resolver, rawContactId);
selectionArg = String.valueOf(contactId);
mSelection = RawContacts.CONTACT_ID + "=?";
}
} else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
final long rawContactId = ContentUris.parseId(data);
selectionArg = String.valueOf(rawContactId);
mSelection = Data.RAW_CONTACT_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.
return RawContactDeltaList.fromQuery(RawContactsEntity.CONTENT_URI,
activityTarget.getContentResolver(), mSelection,
new String[] { selectionArg }, null);
}
private static long queryForContactId(ContentResolver resolver, long rawContactId) {
Cursor contactIdCursor = null;
long contactId = -1;
try {
contactIdCursor = resolver.query(RawContacts.CONTENT_URI,
new String[] { RawContacts.CONTACT_ID },
RawContacts._ID + "=?", new String[] { String.valueOf(rawContactId) },
null);
if (contactIdCursor != null && contactIdCursor.moveToFirst()) {
contactId = contactIdCursor.getLong(0);
}
} finally {
if (contactIdCursor != null) {
contactIdCursor.close();
}
}
return contactId;
}
@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 = Contacts.getLookupUri(mContactId,
cursor.getString(ContactQuery.LOOKUP_KEY));
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(
editableAccount.type, editableAccount.dataSet);
// Create a new RawContactDelta for the new raw_contact.
final RawContact rawContact = new RawContact(context);
rawContact.setAccount(editableAccount);
final RawContactDelta entityDelta = new RawContactDelta(ValuesDelta.fromAfter(
rawContact.getValues()));
// Then, copy the structure name from an existing (read-only) raw_contact.
for (RawContactDelta entity : entityDeltaList) {
final ArrayList<ValuesDelta> readOnlyNames =
entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) {
final ValuesDelta readOnlyName = readOnlyNames.get(0);
final 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 (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, 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 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(ContactsContract.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();
}
}