Display error when failed to link some contacts

SIM Raw contacts are unlinkable on some OEM devices
like samsung s4 and s5. Their native contact app disables
the feature but we still support it for Nexus and newer
devices like s7. Show error toast when failing to link
these contacts instead of showing a success toast.

Test: manual
 - Link "unlinkable" contacts and see new error message
 - Link linkable contacts and see success path
 - Link sim contacts and see error message on s5

Bug:28637870
Bug:28622373

Change-Id: Ia124f4f2a842c8b8ad3b57367a19edea9fc0fbab
diff --git a/res/values/strings.xml b/res/values/strings.xml
index becab16..0f9f16e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -300,19 +300,19 @@
     <string name="contactUnlinkedToast">Contacts unlinked</string>
 
     <!-- Toast displayed when saving a contact failed. [CHAR LIMIT=NONE] -->
-    <string name="contactSavedErrorToast">Couldn\'t save contact changes.</string>
+    <string name="contactSavedErrorToast">Couldn\'t save contact changes</string>
 
     <!-- Toast displayed when unlinking a contact failed. [CHAR LIMIT=NONE] -->
-    <string name="contactUnlinkErrorToast">Couldn\'t unlink contact.</string>
+    <string name="contactUnlinkErrorToast">Couldn\'t unlink contact</string>
 
     <!-- Toast displayed when linking a contact failed. [CHAR LIMIT=NONE] -->
-    <string name="contactJoinErrorToast">Couldn\'t link contact.</string>
+    <string name="contactJoinErrorToast">Couldn\'t link contact</string>
 
     <!-- Generic error default clause displayed when saving a contact failed. [CHAR LIMIT=NONE] -->
-    <string name="contactGenericErrorToast">Error saving contact.</string>
+    <string name="contactGenericErrorToast">Error saving contact</string>
 
     <!-- Toast displayed when saving a contact photo failed. [CHAR LIMIT=NONE] -->
-    <string name="contactPhotoSavedErrorToast">Couldn\'t save contact photo changes.</string>
+    <string name="contactPhotoSavedErrorToast">Couldn\'t save contact photo changes</string>
 
     <!-- Toast displayed when something goes wrong while loading a label. [CHAR LIMIT=70] -->
     <string name="groupLoadErrorToast">Failed to load label</string>
@@ -339,7 +339,7 @@
     <string name="groupMembersAddedToast">Added to label</string>
 
     <!-- Toast displayed when saving a label failed [CHAR LIMIT=70] -->
-    <string name="groupSavedErrorToast">Couldn\'t save label changes.</string>
+    <string name="groupSavedErrorToast">Couldn\'t save label changes</string>
 
     <!-- Message displayed when creating a group with the same name as an existing group -->
     <string name="groupExistsErrorMessage">That label already exists</string>
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index b34f384..c514a45 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -1359,17 +1359,26 @@
             return;
         }
 
-        if (receiver != null) {
-            final Bundle result = new Bundle();
-            result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
-            result.putString(EXTRA_DISPLAY_NAME, queryNameOfLinkedContacts(contactIds));
-            receiver.send(CONTACTS_LINKED, result);
+
+        final String name = queryNameOfLinkedContacts(contactIds);
+        if (name != null) {
+            if (receiver != null) {
+                final Bundle result = new Bundle();
+                result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
+                result.putString(EXTRA_DISPLAY_NAME, name);
+                receiver.send(CONTACTS_LINKED, result);
+            } else {
+                showToast(R.string.contactsJoinedMessage);
+            }
         } else {
-            showToast(R.string.contactsJoinedMessage);
+            if (receiver != null) {
+                receiver.send(CP2_ERROR, new Bundle());
+            }
+            showToast(R.string.contactJoinErrorToast);
         }
     }
 
-    // Get the display name of the top-level contact after the contacts have been linked.
+    /** Get the display name of the top-level contact after the contacts have been linked. */
     private String queryNameOfLinkedContacts(long[] contactIds) {
         final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
         final String[] whereArgs = new String[contactIds.length];
@@ -1379,23 +1388,41 @@
         }
         whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
         final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
-                new String[]{Contacts.DISPLAY_NAME}, whereBuilder.toString(), whereArgs, null);
+                new String[]{Contacts._ID, Contacts.DISPLAY_NAME},
+                whereBuilder.toString(), whereArgs, null);
+
+        String name = null;
+        long contactId = 0;
         try {
             if (cursor.moveToFirst()) {
-                return cursor.getString(0);
+                contactId = cursor.getLong(0);
+                name = cursor.getString(1);
             }
-            return null;
+            while(cursor.moveToNext()) {
+                if (cursor.getLong(0) != contactId) {
+                    return null;
+                }
+            }
+            return name == null ? "" : name;
         } finally {
-            cursor.close();
+            if (cursor != null) {
+                cursor.close();
+            }
         }
     }
 
-
     /** Returns true if the batch was successfully applied and false otherwise. */
     private boolean applyOperations(ContentResolver resolver,
             ArrayList<ContentProviderOperation> operations) {
         try {
-            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+            final ContentProviderResult[] result =
+                    resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+            for (int i = 0; i < result.length; ++i) {
+                // if no rows were modified in the operation then we count it as fail.
+                if (result[i].count < 0) {
+                    throw new OperationApplicationException();
+                }
+            }
             return true;
         } catch (RemoteException | OperationApplicationException e) {
             Log.e(TAG, "Failed to apply aggregation exception batch", e);
@@ -1460,19 +1487,12 @@
             operations.add(builder.build());
         }
 
-        boolean success = false;
         // Apply all aggregation exceptions as one batch
-        try {
-            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
-            showToast(R.string.contactsJoinedMessage);
-            success = true;
-        } catch (RemoteException | OperationApplicationException e) {
-            Log.e(TAG, "Failed to apply aggregation exception batch", e);
-            showToast(R.string.contactSavedErrorToast);
-        }
+        final boolean success = applyOperations(resolver, operations);
 
+        final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
-        if (success) {
+        if (success && name != null) {
             Uri uri = RawContacts.getContactLookupUri(resolver,
                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
             callbackIntent.setData(uri);