Support adding phone/email to readonly contacts

If the selected contact has no editable raw_contacts, create one on the
first editable account found, and add the data to it.

We'll reuse the "contact is readonly" mode if there's no editable accounts
on the system.  We could improve the message, but if there's no editable
accounts we shouldn't show the contact picker in the first place, so I
didn't change the message.  Let's fix the picker instead.

Tested:
(Previously supported casses)
- Add a phone number to an existing editable contact.
- Add a phone number to an existing local-only contact.

(Newly supported cases)
- Add a phone number to an existing read-only contact.
- Try to add a phone number to an existing read-only contact, but no
editable accounts are set up.

Bug 5462347

Change-Id: I55db01610cebb60d3830e8ec4ac6bf1a3dfdad88
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index f98e47b..becbf5c 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -21,6 +21,7 @@
 import com.android.contacts.editor.ViewIdGenerator;
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountWithDataSet;
 import com.android.contacts.model.DataKind;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
@@ -37,6 +38,7 @@
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.OperationApplicationException;
@@ -54,6 +56,7 @@
 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;
@@ -73,6 +76,7 @@
 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
@@ -82,6 +86,18 @@
  * {@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 {
@@ -413,6 +429,12 @@
             if (activityTarget.isFinishing()) {
                 return;
             }
+            if ((entityList == null) || (entityList.size() == 0)) {
+                Log.e(TAG, "Contact not found.");
+                activityTarget.finish();
+                return;
+            }
+
             activityTarget.setEntityDeltaList(entityList);
         }
     }
@@ -555,9 +577,20 @@
 
         mEntityDeltaList = entityList;
 
-        // Find the editable type.
+        // Find the editable raw_contact.
         mEntityDelta = mEntityDeltaList.getFirstWritableRawContact(this);
+
+        // If no editable raw_contacts are found, create one.
         if (mEntityDelta == null) {
+            mEntityDelta = addEditableRawContact(this, mEntityDeltaList);
+
+            if ((mEntityDelta != null) && VERBOSE_LOGGING) {
+                Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList);
+            }
+        }
+
+        if (mEntityDelta == null) {
+            // Selected contact is read-only, and there's no editable account.
             mIsReadOnly = true;
             mEditableAccountType = null;
         } else {
@@ -577,6 +610,67 @@
     }
 
     /**
+     * Create an {@link EntityDelta} 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 EntityDelta addEditableRawContact(Context context,
+            EntityDeltaList 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 EntityDelta for the new raw_contact.
+        final ContentValues values = new ContentValues();
+        values.put(RawContacts.ACCOUNT_NAME, editableAccount.name);
+        values.put(RawContacts.ACCOUNT_TYPE, editableAccount.type);
+        values.put(RawContacts.DATA_SET, editableAccount.dataSet);
+
+        final EntityDelta entityDelta = new EntityDelta(ValuesDelta.fromAfter(values));
+
+        // Then, copy the structure name from an existing (read-only) raw_contact.
+        for (EntityDelta 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 = EntityModifier.ensureKindExists(entityDelta,
+                        accountType, StructuredName.CONTENT_ITEM_TYPE);
+
+                // Copy all the data fields.
+                newName.copyStringFrom(readOnlyName, StructuredName.DISPLAY_NAME);
+
+                newName.copyStringFrom(readOnlyName, StructuredName.GIVEN_NAME);
+                newName.copyStringFrom(readOnlyName, StructuredName.FAMILY_NAME);
+                newName.copyStringFrom(readOnlyName, StructuredName.PREFIX);
+                newName.copyStringFrom(readOnlyName, StructuredName.MIDDLE_NAME);
+                newName.copyStringFrom(readOnlyName, StructuredName.SUFFIX);
+
+                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_GIVEN_NAME);
+                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_MIDDLE_NAME);
+                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_FAMILY_NAME);
+
+                newName.copyStringFrom(readOnlyName, StructuredName.FULL_NAME_STYLE);
+                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_NAME_STYLE);
+                break;
+            }
+        }
+
+        // Add the new EntityDelta to the list.
+        entityDeltaList.add(entityDelta);
+
+        return entityDelta;
+    }
+
+    /**
      * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object.
      */
     private void bindEditor() {
@@ -724,6 +818,9 @@
             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()) {
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index bc6ba59..0f45a4b 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -781,6 +781,11 @@
             mAfter.putNull(key);
         }
 
+        public void copyStringFrom(ValuesDelta from, String key) {
+            ensureUpdate();
+            put(key, from.getAsString(key));
+        }
+
         /**
          * Return set of all keys defined through this object.
          */
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 6db9e8b..be6b5a7 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -46,6 +46,8 @@
         <item>ACTION_GET_CONTENT: postal</item>
         <item>ACTION_GET_CONTENT: postal (legacy)</item>
         <item>ACTION_INSERT_OR_EDIT</item>
+        <item>ACTION_INSERT_OR_EDIT_PHONE_NUMBER</item>
+        <item>ACTION_INSERT_OR_EDIT_EMAIL_ADDRESS</item>
         <item>ACTION_SEARCH (call button)</item>
         <item>ACTION_SEARCH: contact</item>
         <item>ACTION_SEARCH: email</item>
diff --git a/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
index 0d9383f..c6577b8 100644
--- a/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
+++ b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
@@ -90,6 +90,8 @@
         ACTION_GET_CONTENT_POSTAL,
         ACTION_GET_CONTENT_POSTAL_LEGACY,
         ACTION_INSERT_OR_EDIT,
+        ACTION_INSERT_OR_EDIT_PHONE_NUMBER,
+        ACTION_INSERT_OR_EDIT_EMAIL_ADDRESS,
         ACTION_SEARCH_CALL,
         ACTION_SEARCH_CONTACT,
         ACTION_SEARCH_EMAIL,
@@ -275,6 +277,20 @@
                 startActivity(intent);
                 break;
             }
+            case ACTION_INSERT_OR_EDIT_PHONE_NUMBER: {
+                Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                intent.putExtra(Insert.PHONE, "5123456789");
+                startActivity(intent);
+                break;
+            }
+            case ACTION_INSERT_OR_EDIT_EMAIL_ADDRESS: {
+                Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                intent.putExtra(Insert.EMAIL, "android@android.com");
+                startActivity(intent);
+                break;
+            }
             case ACTION_SEARCH_CALL: {
                 Intent intent = new Intent(Intent.ACTION_SEARCH);
                 intent.putExtra(SearchManager.ACTION_MSG, "call");