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);
         }
     };