Merge "Guard against a Monkey NPE"
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index c0399e4..5f5c1cb 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -1297,6 +1297,9 @@
* new result will be delivered
*/
public void upgradeToFullContact() {
+ // Everything requested already? Nothing to do, so let's bail out
+ if (mLoadGroupMetaData && mLoadInvitableAccountTypes && mLoadStreamItems) return;
+
mLoadGroupMetaData = true;
mLoadInvitableAccountTypes = true;
mLoadStreamItems = true;
@@ -1346,6 +1349,6 @@
* contact. If the next load is for a different contact, the cached result will be dropped
*/
public void cacheResult() {
- sCachedResult = mContact;
+ sCachedResult = new Result(mContact);
}
}
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index aa3be87..f98e47b 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -78,17 +78,16 @@
* 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 ContactsContract.Intents.Insert.PHONE} with type
- * {@link ContactsContract.Intents.Insert.PHONE_TYPE} or
- * {@link ContactsContract.Intents.Insert.EMAIL} with type
- * {@link ContactsContract.Intents.Insert.EMAIL_TYPE} intent keys.
+ * {@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.
*/
public class ConfirmAddDetailActivity extends Activity implements
DialogManager.DialogShowingViewActivity {
- private static final String TAG = ConfirmAddDetailActivity.class.getSimpleName();
-
- private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
+ 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;
@@ -102,15 +101,19 @@
private ContentResolver mContentResolver;
private AccountType mEditableAccountType;
- private EntityDelta mState;
private Uri mContactUri;
private long mContactId;
private String mDisplayName;
- private boolean mIsReadyOnly;
+ private boolean mIsReadOnly;
private QueryHandler mQueryHandler;
+
+ /** {@link EntityDeltaList} for the entire selected contact. */
private EntityDeltaList mEntityDeltaList;
+ /** {@link EntityDeltaList} for the editable account */
+ private EntityDelta mEntityDelta;
+
private String mMimetype = Phone.CONTENT_ITEM_TYPE;
/**
@@ -168,9 +171,9 @@
* a disambiguation case. For example, if the contact does not have a
* nickname, use the email field, and etc.
*/
- private static final String[] sMimeTypePriorityList = 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 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;
@@ -180,7 +183,7 @@
private final OnClickListener mDetailsButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
- if (mIsReadyOnly) {
+ if (mIsReadOnly) {
onSaveCompleted(true);
} else {
doSaveAction();
@@ -250,7 +253,8 @@
mPhotoView = (ImageView) findViewById(R.id.photo);
mEditorContainerView = (ViewGroup) findViewById(R.id.editor_container);
- startContactQuery(mContactUri, true);
+ resetAsyncQueryHandler();
+ startContactQuery(mContactUri);
new QueryEntitiesTask(this).execute(intent);
}
@@ -282,13 +286,8 @@
* Internal method to query contact by Uri.
*
* @param contactUri the contact uri
- * @param resetQueryHandler whether to use a new AsyncQueryHandler or not
*/
- private void startContactQuery(Uri contactUri, boolean resetQueryHandler) {
- if (resetQueryHandler) {
- resetAsyncQueryHandler();
- }
-
+ private void startContactQuery(Uri contactUri) {
mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
null, null, null);
}
@@ -298,13 +297,8 @@
*
* @param photoId the photo id.
* @param lookupKey the lookup uri.
- * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
*/
- private void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) {
- if (resetQueryHandler) {
- resetAsyncQueryHandler();
- }
-
+ private void startPhotoQuery(long photoId, Uri lookupKey) {
mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
PhotoQuery.COLUMNS, null, null, null);
@@ -420,9 +414,6 @@
return;
}
activityTarget.setEntityDeltaList(entityList);
- activityTarget.findEditableRawContact();
- activityTarget.parseExtras();
- activityTarget.bindEditor();
}
}
@@ -477,12 +468,11 @@
// Otherwise do the photo query.
Uri lookupUri = Contacts.getLookupUri(mContactId,
cursor.getString(ContactQuery.LOOKUP_KEY));
- startPhotoQuery(photoId, lookupUri,
- false /* don't reset query handler */);
+ startPhotoQuery(photoId, lookupUri);
// Display the name because there is no
// disambiguation query.
setDisplayName();
- onLoadDataFinished();
+ showDialogContent();
}
}
break;
@@ -500,7 +490,7 @@
// If there are no other contacts with this name,
// then display the name.
setDisplayName();
- onLoadDataFinished();
+ showDialogContent();
}
break;
}
@@ -535,14 +525,14 @@
// 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 : sMimeTypePriorityList) {
+ for (String mimeType : MIME_TYPE_PRIORITY_LIST) {
if (hashMapCursorData.containsKey(mimeType)) {
setDisplayName();
setExtraInfoField(hashMapCursorData.get(mimeType));
break;
}
}
- onLoadDataFinished();
+ showDialogContent();
}
break;
}
@@ -555,28 +545,35 @@
}
}
- public void setEntityDeltaList(EntityDeltaList entityList) {
+ private void setEntityDeltaList(EntityDeltaList entityList) {
+ if (entityList == null) {
+ throw new IllegalStateException();
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "setEntityDeltaList: " + entityList);
+ }
+
mEntityDeltaList = entityList;
- }
- public void findEditableRawContact() {
- if (mEntityDeltaList == null) return;
- mState = mEntityDeltaList.getFirstWritableRawContact(this);
- if (mState != null) {
- mEditableAccountType = mState.getRawContactAccountType(this);
- }
- }
+ // Find the editable type.
+ mEntityDelta = mEntityDeltaList.getFirstWritableRawContact(this);
+ if (mEntityDelta == null) {
+ mIsReadOnly = true;
+ mEditableAccountType = null;
+ } else {
+ mIsReadOnly = false;
- public void parseExtras() {
- if (mEditableAccountType == null || mState == null) {
- return;
+ mEditableAccountType = mEntityDelta.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 EntityDelta.
+ EntityModifier.parseExtras(this, mEditableAccountType, mEntityDelta, extras);
+ }
}
- // 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 EntityDelta.
- EntityModifier.parseExtras(this, mEditableAccountType, mState, extras);
- }
+
+ bindEditor();
}
/**
@@ -584,19 +581,18 @@
*/
private void bindEditor() {
if (mEntityDeltaList == null) {
- return;
+ 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 (mEditableAccountType == null) {
- mIsReadyOnly = true;
+ 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
- onLoadDataFinished();
+ showDialogContent();
return;
}
@@ -605,11 +601,11 @@
// Skip kind that are not editable
if (!kind.editable) continue;
if (mMimetype.equals(kind.mimeType)) {
- for (ValuesDelta valuesDelta : mState.getMimeEntries(mMimetype)) {
+ for (ValuesDelta valuesDelta : mEntityDelta.getMimeEntries(mMimetype)) {
// Skip entries that aren't visible
if (!valuesDelta.isVisible()) continue;
if (valuesDelta.isInsert()) {
- inflateEditorView(kind, valuesDelta, mState);
+ inflateEditorView(kind, valuesDelta, mEntityDelta);
return;
}
}
@@ -660,7 +656,7 @@
* once all the queries have completed, otherwise the screen will flash as additional data
* comes in.
*/
- private void onLoadDataFinished() {
+ private void showDialogContent() {
mRootView.setVisibility(View.VISIBLE);
}
@@ -673,14 +669,13 @@
task.execute(mEntityDeltaList);
}
-
/**
* Background task for persisting edited contact data, using the changes
* defined by a set of {@link EntityDelta}. This task starts
* {@link EmptyService} to make sure the background thread can finish
* persisting in cases where the system wants to reclaim our process.
*/
- public static class PersistTask extends AsyncTask<EntityDeltaList, Void, Integer> {
+ private static class PersistTask extends AsyncTask<EntityDeltaList, 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;
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 2620fb0..bc6ba59 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -78,7 +78,7 @@
* Internal map of children values from {@link Entity#getSubValues()}, which
* we store here sorted into {@link Data#MIMETYPE} bins.
*/
- private HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
+ private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
public EntityDelta() {
}
@@ -354,15 +354,18 @@
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("\n(");
+ builder.append("Uri=");
+ builder.append(mContactsQueryUri);
+ builder.append(", Values=");
builder.append(mValues != null ? mValues.toString() : "null");
- builder.append(") = {");
+ builder.append(", Entries={");
for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
for (ValuesDelta child : mimeEntries) {
builder.append("\n\t");
child.toString(builder);
}
}
- builder.append("\n}\n");
+ builder.append("\n})\n");
return builder.toString();
}
@@ -865,6 +868,11 @@
*/
public void toString(StringBuilder builder) {
builder.append("{ ");
+ builder.append("IdColumn=");
+ builder.append(mIdColumn);
+ builder.append(", FromTemplate=");
+ builder.append(mFromTemplate);
+ builder.append(", ");
for (String key : this.keySet()) {
builder.append(key);
builder.append("=");
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/EntityDeltaList.java
index 478c879..47fd9c6 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/EntityDeltaList.java
@@ -16,24 +16,25 @@
package com.android.contacts.model;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
+
import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Entity;
import android.content.EntityIterator;
-import android.content.ContentProviderOperation.Builder;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
-
-import com.google.android.collect.Lists;
-
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import android.util.Log;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
/**
@@ -42,6 +43,9 @@
* and applying another {@link EntityDeltaList} over it.
*/
public class EntityDeltaList extends ArrayList<EntityDelta> implements Parcelable {
+ private static final String TAG = "EntityDeltaList";
+ private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
private boolean mSplitRawContacts;
private long[] mJoinWithRawContactIds;
@@ -123,6 +127,9 @@
* any {@link AggregationExceptions} rules needed to groups edits together.
*/
public ArrayList<ContentProviderOperation> buildDiff() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "buildDiff: list=" + toString());
+ }
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
final long rawContactId = this.findRawContactId();
@@ -197,10 +204,23 @@
if (diff.size() == assertMark) {
diff.clear();
}
-
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "buildDiff: ops=" + diffToString(diff));
+ }
return diff;
}
+ private static String diffToString(ArrayList<ContentProviderOperation> ops) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[\n");
+ for (ContentProviderOperation op : ops) {
+ sb.append(op.toString());
+ sb.append(",\n");
+ }
+ sb.append("]\n");
+ return sb.toString();
+ }
+
/**
* Start building a {@link ContentProviderOperation} that will keep two
* {@link RawContacts} together.
@@ -416,4 +436,18 @@
return new EntityDeltaList[size];
}
};
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ sb.append("Split=");
+ sb.append(mSplitRawContacts);
+ sb.append(", Join=[");
+ sb.append(Arrays.toString(mJoinWithRawContactIds));
+ sb.append("], Values=");
+ sb.append(super.toString());
+ sb.append(")");
+ return sb.toString();
+ }
}
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 7087167..11f3da1 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -31,7 +31,6 @@
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.LoaderManager.LoaderCallbacks;
-import android.app.TaskStackBuilder;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -225,7 +224,8 @@
mPhotoContainer = findViewById(R.id.photo_container);
setHeaderNameText(R.id.name, R.string.missing_name);
- getLoaderManager().initLoader(LOADER_ID, null, mLoaderCallbacks);
+ mContactLoader = (ContactLoader) getLoaderManager().initLoader(
+ LOADER_ID, null, mLoaderCallbacks);
}
private boolean handleOutsideTouch() {
@@ -523,8 +523,7 @@
if (mLookupUri == null) {
Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
}
- mContactLoader = new ContactLoader(getApplicationContext(), mLookupUri);
- return mContactLoader;
+ return new ContactLoader(getApplicationContext(), mLookupUri);
}
};