Pushing contact joining to the service (background thread)

Bug: 3228687
Change-Id: If7f492aab683e6033a82e45714795c4a04407234
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index a8df2e0..2f9a514 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -40,6 +40,8 @@
         DialogManager.DialogShowingViewActivity {
     private static final String TAG = "ContactEditorActivity";
 
+    public static final String ACTION_JOIN_COMPLETED = "joinCompleted";
+
     private ContactEditorFragment mFragment;
     private Button mDoneButton;
     private Button mRevertButton;
@@ -80,8 +82,15 @@
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
 
-        if (Intent.ACTION_EDIT.equals(intent.getAction()) && mFragment != null){
+        if (mFragment == null) {
+            return;
+        }
+
+        String action = intent.getAction();
+        if (Intent.ACTION_EDIT.equals(action)) {
             mFragment.setIntentExtras(intent.getExtras());
+        } else if (ACTION_JOIN_COMPLETED.equals(action)) {
+            mFragment.onJoinCompleted(intent.getData());
         }
     }
 
diff --git a/src/com/android/contacts/views/ContactSaveService.java b/src/com/android/contacts/views/ContactSaveService.java
index 7e88f15..78aa428 100644
--- a/src/com/android/contacts/views/ContactSaveService.java
+++ b/src/com/android/contacts/views/ContactSaveService.java
@@ -16,26 +16,33 @@
 
 package com.android.contacts.views;
 
+import com.android.contacts.R;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
 
 import android.accounts.Account;
 import android.app.IntentService;
 import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
 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;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
+import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -68,6 +75,11 @@
     public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
     public static final String EXTRA_DATA_ID = "dataId";
 
+    public static final String ACTION_JOIN_CONTACTS = "joinContacts";
+    public static final String EXTRA_CONTACT_ID1 = "contactId1";
+    public static final String EXTRA_CONTACT_ID2 = "contactId2";
+    public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
+
     private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
         Data.MIMETYPE,
         Data.IS_PRIMARY,
@@ -112,6 +124,8 @@
             clearPrimary(intent);
         } else if (ACTION_DELETE_CONTACT.equals(action)) {
             deleteContact(intent);
+        } else if (ACTION_JOIN_CONTACTS.equals(action)) {
+            joinContacts(intent);
         }
     }
 
@@ -372,4 +386,151 @@
 
         getContentResolver().delete(contactUri, null, null);
     }
+
+    /**
+     * Creates an intent that can be sent to this service to join two contacts.
+     */
+    public static Intent createJoinContactsIntent(Context context, long contactId1,
+            long contactId2, boolean contactWritable,
+            Class<?> 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);
+        callbackIntent.setAction(callbackAction);
+        callbackIntent.setFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+
+        return serviceIntent;
+    }
+
+
+    private interface JoinContactQuery {
+        String[] PROJECTION = {
+                RawContacts._ID,
+                RawContacts.CONTACT_ID,
+                RawContacts.NAME_VERIFIED,
+                RawContacts.DISPLAY_NAME_SOURCE,
+        };
+
+        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
+
+        int _ID = 0;
+        int CONTACT_ID = 1;
+        int NAME_VERIFIED = 2;
+        int DISPLAY_NAME_SOURCE = 3;
+    }
+
+    private void joinContacts(Intent intent) {
+        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
+        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
+        boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
+        if (contactId1 == -1 || contactId2 == -1) {
+            Log.e(TAG, "Invalid arguments for joinContacts request");
+            return;
+        }
+
+        final ContentResolver resolver = getContentResolver();
+
+        // Load raw contact IDs for all raw contacts involved - currently edited and selected
+        // in the join UIs
+        Cursor c = resolver.query(RawContacts.CONTENT_URI,
+                JoinContactQuery.PROJECTION,
+                JoinContactQuery.SELECTION,
+                new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
+
+        long rawContactIds[];
+        long verifiedNameRawContactId = -1;
+        try {
+            int maxDisplayNameSource = -1;
+            rawContactIds = new long[c.getCount()];
+            for (int i = 0; i < rawContactIds.length; i++) {
+                c.moveToPosition(i);
+                long rawContactId = c.getLong(JoinContactQuery._ID);
+                rawContactIds[i] = rawContactId;
+                int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
+                if (nameSource > maxDisplayNameSource) {
+                    maxDisplayNameSource = nameSource;
+                }
+            }
+
+            // Find an appropriate display name for the joined contact:
+            // if should have a higher DisplayNameSource or be the name
+            // of the original contact that we are joining with another.
+            if (writable) {
+                for (int i = 0; i < rawContactIds.length; i++) {
+                    c.moveToPosition(i);
+                    if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
+                        int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
+                        if (nameSource == maxDisplayNameSource
+                                && (verifiedNameRawContactId == -1
+                                        || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
+                            verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
+                        }
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        // For each pair of raw contacts, insert an aggregation exception
+        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]);
+                }
+            }
+        }
+
+        // Mark the original contact as "name verified" to make sure that the contact
+        // display name does not change as a result of the join
+        if (verifiedNameRawContactId != -1) {
+            Builder builder = ContentProviderOperation.newUpdate(
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
+            builder.withValue(RawContacts.NAME_VERIFIED, 1);
+            operations.add(builder.build());
+        }
+
+        boolean success = false;
+        // Apply all aggregation exceptions as one batch
+        try {
+            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+            Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
+            success = true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        } catch (OperationApplicationException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        }
+
+        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+        if (success) {
+            Uri uri = RawContacts.getContactLookupUri(resolver,
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
+            callbackIntent.setData(uri);
+        }
+        startActivity(callbackIntent);
+    }
+
+    /**
+     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
+     */
+    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
+            long rawContactId1, long rawContactId2) {
+        Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        operations.add(builder.build());
+    }
 }
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
index 668fd04..2685b0b 100644
--- a/src/com/android/contacts/views/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -17,6 +17,7 @@
 package com.android.contacts.views.editor;
 
 import com.android.contacts.R;
+import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.JoinContactActivity;
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypes;
@@ -28,6 +29,7 @@
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
 import com.android.contacts.views.ContactLoader;
+import com.android.contacts.views.ContactSaveService;
 import com.android.contacts.views.GroupMetaDataLoader;
 import com.android.contacts.views.editor.AggregationSuggestionEngine.Suggestion;
 import com.android.contacts.views.editor.Editor.EditorListener;
@@ -43,7 +45,6 @@
 import android.app.ProgressDialog;
 import android.content.ActivityNotFoundException;
 import android.content.ContentProviderOperation;
-import android.content.ContentProviderOperation.Builder;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -65,7 +66,6 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.provider.ContactsContract;
-import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.Note;
@@ -737,7 +737,11 @@
         return true;
     }
 
-    private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
+    public void onJoinCompleted(Uri uri) {
+        onSaveCompleted(uri != null, SaveMode.RELOAD, uri);
+    }
+
+    public void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
         Log.d(TAG, "onSaveCompleted(" + success + ", " + saveMode + ", " + contactLookupUri);
         switch (saveMode) {
             case SaveMode.CLOSE:
@@ -816,106 +820,14 @@
         startActivityForResult(intent, REQUEST_CODE_JOIN);
     }
 
-    private interface JoinContactQuery {
-        String[] PROJECTION = {
-                RawContacts._ID,
-                RawContacts.CONTACT_ID,
-                RawContacts.NAME_VERIFIED,
-                RawContacts.DISPLAY_NAME_SOURCE,
-        };
-
-        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
-
-        int _ID = 0;
-        int CONTACT_ID = 1;
-        int NAME_VERIFIED = 2;
-        int DISPLAY_NAME_SOURCE = 3;
-    }
-
     /**
      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
      */
     private void joinAggregate(final long contactId) {
-        final ContentResolver resolver = mContext.getContentResolver();
-
-        // Load raw contact IDs for all raw contacts involved - currently edited and selected
-        // in the join UIs
-        Cursor c = resolver.query(RawContacts.CONTENT_URI,
-                JoinContactQuery.PROJECTION,
-                JoinContactQuery.SELECTION,
-                new String[]{String.valueOf(contactId), String.valueOf(mContactIdForJoin)}, null);
-
-        long rawContactIds[];
-        long verifiedNameRawContactId = -1;
-        try {
-            int maxDisplayNameSource = -1;
-            rawContactIds = new long[c.getCount()];
-            for (int i = 0; i < rawContactIds.length; i++) {
-                c.moveToPosition(i);
-                long rawContactId = c.getLong(JoinContactQuery._ID);
-                rawContactIds[i] = rawContactId;
-                int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
-                if (nameSource > maxDisplayNameSource) {
-                    maxDisplayNameSource = nameSource;
-                }
-            }
-
-            // Find an appropriate display name for the joined contact:
-            // if should have a higher DisplayNameSource or be the name
-            // of the original contact that we are joining with another.
-            if (isContactWritable()) {
-                for (int i = 0; i < rawContactIds.length; i++) {
-                    c.moveToPosition(i);
-                    if (c.getLong(JoinContactQuery.CONTACT_ID) == mContactIdForJoin) {
-                        int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
-                        if (nameSource == maxDisplayNameSource
-                                && (verifiedNameRawContactId == -1
-                                        || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
-                            verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
-                        }
-                    }
-                }
-            }
-        } finally {
-            c.close();
-        }
-
-        // For each pair of raw contacts, insert an aggregation exception
-        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]);
-                }
-            }
-        }
-
-        // Mark the original contact as "name verified" to make sure that the contact
-        // display name does not change as a result of the join
-        if (verifiedNameRawContactId != -1) {
-            Builder builder = ContentProviderOperation.newUpdate(
-                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
-            builder.withValue(RawContacts.NAME_VERIFIED, 1);
-            operations.add(builder.build());
-        }
-
-        boolean success = false;
-        // Apply all aggregation exceptions as one batch
-        try {
-            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
-            Toast.makeText(mContext, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
-            success = true;
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to apply aggregation exception batch", e);
-            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
-        } catch (OperationApplicationException e) {
-            Log.e(TAG, "Failed to apply aggregation exception batch", e);
-            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
-        }
-
-        if (success) {
-            onSaveCompleted(true, SaveMode.RELOAD, mLookupUri);
-        }
+        Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
+                contactId, isContactWritable(),
+                ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
+        mContext.startService(intent);
     }
 
     /**
@@ -936,19 +848,6 @@
         return false;
     }
 
-    /**
-     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
-     */
-    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
-            long rawContactId1, long rawContactId2) {
-        Builder builder =
-                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
-        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
-        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
-        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
-        operations.add(builder.build());
-    }
-
     public static interface Listener {
         /**
          * Contact was not found, so somehow close this fragment. This is raised after a contact