Batch join contacts

* Add new action to ContactSaveService to support joining more than
 two contacts toghether.
* Add new dialog fragment for the join

Bug: 19549465
Change-Id: Ib0b1d5e7652e429f8e78d81dd3d98d03b3129e1e
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 1668521..cf36edf 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -109,9 +109,9 @@
     public static final String EXTRA_DATA_ID = "dataId";
 
     public static final String ACTION_JOIN_CONTACTS = "joinContacts";
+    public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
     public static final String EXTRA_CONTACT_ID1 = "contactId1";
     public static final String EXTRA_CONTACT_ID2 = "contactId2";
-    public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
 
     public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
     public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
@@ -211,6 +211,8 @@
             deleteContact(intent);
         } else if (ACTION_JOIN_CONTACTS.equals(action)) {
             joinContacts(intent);
+        } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
+            joinSeveralContacts(intent);
         } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
             setSendToVoicemail(intent);
         } else if (ACTION_SET_RINGTONE.equals(action)) {
@@ -985,15 +987,14 @@
 
     /**
      * Creates an intent that can be sent to this service to join two contacts.
+     * The resulting contact uses the name from {@param contactId1} if possible.
      */
     public static Intent createJoinContactsIntent(Context context, long contactId1,
-            long contactId2, boolean contactWritable,
-            Class<? extends Activity> callbackActivity, String callbackAction) {
+            long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
-        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
 
         // Callback intent will be invoked by the service once the contacts are joined.
         Intent callbackIntent = new Intent(context, callbackActivity);
@@ -1003,6 +1004,17 @@
         return serviceIntent;
     }
 
+    /**
+     * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
+     * No special attention is paid to where the resulting contact's name is taken from.
+     */
+    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
+        Intent serviceIntent = new Intent(context, ContactSaveService.class);
+        serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
+        return serviceIntent;
+    }
+
 
     private interface JoinContactQuery {
         String[] PROJECTION = {
@@ -1011,8 +1023,6 @@
                 RawContacts.DISPLAY_NAME_SOURCE,
         };
 
-        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
-
         int _ID = 0;
         int CONTACT_ID = 1;
         int DISPLAY_NAME_SOURCE = 2;
@@ -1034,22 +1044,48 @@
         int IS_SUPER_PRIMARY = 2;
     }
 
-    private void joinContacts(Intent intent) {
-        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
-        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
+    private void joinSeveralContacts(Intent intent) {
+        final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
 
-        if (contactId1 == -1 || contactId2 == -1) {
-            Log.e(TAG, "Invalid arguments for joinContacts request");
+        // Load raw contact IDs for all contacts involved.
+        long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
+        if (rawContactIds == null) {
+            Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
             return;
         }
 
+        // For each pair of raw contacts, insert an aggregation exception
         final ContentResolver resolver = getContentResolver();
+        final ArrayList<ContentProviderOperation> operations
+                = new ArrayList<ContentProviderOperation>();
+        for (int i = 0; i < rawContactIds.length; i++) {
+            for (int j = 0; j < rawContactIds.length; j++) {
+                if (i != j) {
+                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
+                }
+            }
+        }
+
+        // Apply all aggregation exceptions as one batch
+        try {
+            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+            showToast(R.string.contactsJoinedMessage);
+        } catch (RemoteException | OperationApplicationException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            showToast(R.string.contactSavedErrorToast);
+        }
+    }
+
+
+    private void joinContacts(Intent intent) {
+        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
+        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
 
         // Load raw contact IDs for all raw contacts involved - currently edited and selected
         // in the join UIs.
         long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
         if (rawContactIds == null) {
-            // Error.
+            Log.e(TAG, "Invalid arguments for joinContacts request");
             return;
         }
 
@@ -1064,6 +1100,8 @@
             }
         }
 
+        final ContentResolver resolver = getContentResolver();
+
         // Use the name for contactId1 as the name for the newly aggregated contact.
         final Uri contactId1Uri = ContentUris.withAppendedId(
                 Contacts.CONTENT_URI, contactId1);
@@ -1101,10 +1139,7 @@
             resolver.applyBatch(ContactsContract.AUTHORITY, operations);
             showToast(R.string.contactsJoinedMessage);
             success = true;
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to apply aggregation exception batch", e);
-            showToast(R.string.contactSavedErrorToast);
-        } catch (OperationApplicationException e) {
+        } catch (RemoteException | OperationApplicationException e) {
             Log.e(TAG, "Failed to apply aggregation exception batch", e);
             showToast(R.string.contactSavedErrorToast);
         }
@@ -1118,13 +1153,32 @@
         deliverCallback(callbackIntent);
     }
 
-    private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
+    private long[] getRawContactIdsForAggregation(long[] contactIds) {
+        if (contactIds == null) {
+            return null;
+        }
+
         final ContentResolver resolver = getContentResolver();
         long rawContactIds[];
+
+        final StringBuilder queryBuilder = new StringBuilder();
+        final String stringContactIds[] = new String[contactIds.length];
+        for (int i = 0; i < contactIds.length; i++) {
+            queryBuilder.append(RawContacts.CONTACT_ID + "=?");
+            stringContactIds[i] = String.valueOf(contactIds[i]);
+            if (contactIds[i] == -1) {
+                return null;
+            }
+            if (i == contactIds.length -1) {
+                break;
+            }
+            queryBuilder.append(" OR ");
+        }
+
         final Cursor c = resolver.query(RawContacts.CONTENT_URI,
                 JoinContactQuery.PROJECTION,
-                JoinContactQuery.SELECTION,
-                new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
+                queryBuilder.toString(),
+                stringContactIds, null);
         if (c == null) {
             Log.e(TAG, "Unable to open Contacts DB cursor");
             showToast(R.string.contactSavedErrorToast);
@@ -1132,7 +1186,7 @@
         }
         try {
             if (c.getCount() < 2) {
-                Log.e(TAG, "Not enough raw contacts to aggregate toghether.");
+                Log.e(TAG, "Not enough raw contacts to aggregate together.");
                 return null;
             }
             rawContactIds = new long[c.getCount()];
@@ -1147,6 +1201,10 @@
         return rawContactIds;
     }
 
+    private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
+        return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
+    }
+
     /**
      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
      */