Merge "Format phone numbers in Recent card call logs" into ub-contactsdialer-g-dev
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-bind/com/android/contactsbind/FeatureHighlightHelper.java b/src-bind/com/android/contactsbind/FeatureHighlightHelper.java
index c246141..be1833d 100644
--- a/src-bind/com/android/contactsbind/FeatureHighlightHelper.java
+++ b/src-bind/com/android/contactsbind/FeatureHighlightHelper.java
@@ -26,4 +26,8 @@
     public static boolean showHamburgerFeatureHighlight(final FragmentActivity activity) {
         return false;
     }
+
+    public static boolean tryRemoveHighlight(final FragmentActivity activity) {
+        return false;
+    }
 }
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);
diff --git a/src/com/android/contacts/activities/GroupMembersActivity.java b/src/com/android/contacts/activities/GroupMembersActivity.java
index e887cb9..c32842a 100644
--- a/src/com/android/contacts/activities/GroupMembersActivity.java
+++ b/src/com/android/contacts/activities/GroupMembersActivity.java
@@ -88,15 +88,17 @@
         private final long mGroupId;
         private final String mAccountName;
         private final String mAccountType;
+        private final String mDataSet;
 
         private UpdateGroupMembersAsyncTask(int type, Context context, long[] contactIds,
-                long groupId, String accountName, String accountType) {
+                long groupId, String accountName, String accountType, String dataSet) {
             mContext = context;
             mType = type;
             mContactIds = contactIds;
             mGroupId = groupId;
             mAccountName = accountName;
             mAccountType = accountType;
+            mDataSet = dataSet;
         }
 
         @Override
@@ -127,10 +129,16 @@
         // ContactSaveService will log a warning if the raw contact is already a member and keep
         // going but it is not ideal, we could also prune raw contacts that are already members.
         private long[] getRawContactIds() {
-            final Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon()
-                    .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccountName)
-                    .appendQueryParameter(RawContacts.ACCOUNT_TYPE, mAccountType)
-                    .build();
+            final Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon();
+            // null account names are not valid, see ContactsProvider2#appendAccountFromParameter
+            if (mAccountName != null) {
+                builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccountName);
+                builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, mAccountType);
+            }
+            if (mDataSet != null) {
+                builder.appendQueryParameter(RawContacts.DATA_SET, mDataSet);
+            }
+            final Uri rawContactUri = builder.build();
             final String[] projection = new String[]{RawContacts._ID};
             final StringBuilder selection = new StringBuilder();
             final String[] selectionArgs = new String[mContactIds.length];
@@ -443,7 +451,7 @@
         final long[] contactIds = mMembersFragment.getAdapter().getSelectedContactIdsArray();
         new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE,
                 this, contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
-                mGroupMetaData.accountType).execute();
+                mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
 
         mActionBarAdapter.setSelectionMode(false);
     }
@@ -489,7 +497,7 @@
             }
             new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_ADD,
                     this, contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
-                    mGroupMetaData.accountType).execute();
+                    mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
         }
     }
 
@@ -610,6 +618,6 @@
         contactIds[0] = contactId;
         new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE,
                 this, contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
-                mGroupMetaData.accountType).execute();
+                mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
     }
 }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index d100d45..166222f 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -1378,6 +1378,8 @@
 
         if (mDrawer.isDrawerOpen(GravityCompat.START)) {
             mDrawer.closeDrawer(GravityCompat.START);
+        } else if (FeatureHighlightHelper.tryRemoveHighlight(this)) {
+            return;
         } else if (mActionBarAdapter.isSelectionMode()) {
             mActionBarAdapter.setSelectionMode(false);
             mAllFragment.displayCheckBoxes(false);
diff --git a/src/com/android/contacts/common/list/ContactListFilter.java b/src/com/android/contacts/common/list/ContactListFilter.java
index 81ee5c9..8e29308 100644
--- a/src/com/android/contacts/common/list/ContactListFilter.java
+++ b/src/com/android/contacts/common/list/ContactListFilter.java
@@ -330,9 +330,12 @@
             throw new IllegalStateException(
                     "filterType must be FILTER_TYPE_ACCOUNT or FILER_TYPE_GROUP_MEMBERS");
         }
-        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
-        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
-        if (!TextUtils.isEmpty(dataSet)) {
+        // null account names are not valid, see ContactsProvider2#appendAccountFromParameter
+        if (accountName != null) {
+            uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
+            uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
+        }
+        if (dataSet != null) {
             uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
         }
         return uriBuilder;
diff --git a/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java b/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
index 0efadc4..8c45c40 100644
--- a/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/common/model/DeviceLocalAccountLocator.java
@@ -78,16 +78,8 @@
 
         // Many device accounts have default groups associated with them.
         addAccountsFromQuery(ContactsContract.Groups.CONTENT_URI, localAccounts);
-
         addAccountsFromQuery(ContactsContract.Settings.CONTENT_URI, localAccounts);
-
-        if (localAccounts.isEmpty()) {
-            // It's probably safe to assume that if one of the earlier queries found a "device"
-            // account then this query isn't going to find any different device accounts.
-            // We skip this query because it probably is kind of expensive (relative to the other
-            // queries).
-            addAccountsFromQuery(ContactsContract.RawContacts.CONTENT_URI, localAccounts);
-        }
+        addAccountsFromQuery(ContactsContract.RawContacts.CONTENT_URI, localAccounts);
 
         return new ArrayList<>(localAccounts);
     }
diff --git a/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java b/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java
index f1a2714..e8c4e2f 100644
--- a/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java
+++ b/tests/src/com/android/contacts/common/model/DeviceLocalAccountLocatorTests.java
@@ -140,24 +140,6 @@
         assertEquals(2, sut.getDeviceLocalAccounts().size());
     }
 
-    public void test_getDeviceLocalAccounts_onlyQueriesRawContactsIfNecessary() {
-        final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
-                .withDeviceTypes(null, "vnd.sec.contact.phone")
-                .withSimTypes("vnd.sec.contact.sim");
-        final FakeContactsProvider contactsProvider = new FakeContactsProvider()
-                .withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
-                        "phone_account", "vnd.sec.contact.phone",
-                        "sim_account", "vnd.sec.contact.sim"
-                ));
-        final DeviceLocalAccountLocator sut = new DeviceLocalAccountLocator(
-                createContentResolverWithProvider(contactsProvider), stubFactory,
-                Collections.<AccountWithDataSet>emptyList());
-
-        sut.getDeviceLocalAccounts();
-
-        assertEquals(0, contactsProvider.getQueryCountFor(RawContacts.CONTENT_URI));
-    }
-
     private DeviceLocalAccountLocator createWithQueryResult(
             Cursor cursor) {
         final DeviceLocalAccountLocator locator = new DeviceLocalAccountLocator(
@@ -194,7 +176,6 @@
     private static class FakeContactsProvider extends MockContentProvider {
         public Cursor mNextQueryResult;
         public Map<Uri, Cursor> mNextResultMapping = new HashMap<>();
-        public Map<Uri, Integer> mQueryCountMapping = new HashMap<>();
 
         public FakeContactsProvider() {}
 
@@ -214,17 +195,10 @@
             return query(uri, projection, selection, selectionArgs, sortOrder, null);
         }
 
-        public int getQueryCountFor(Uri uri) {
-            ensureCountInitialized(uri);
-            return mQueryCountMapping.get(uri);
-        }
-
         @Nullable
         @Override
         public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                 String sortOrder, CancellationSignal cancellationSignal) {
-            incrementQueryCount(uri);
-
             final Cursor result = mNextResultMapping.get(uri);
             if (result == null) {
                 return mNextQueryResult;
@@ -232,17 +206,5 @@
                 return result;
             }
         }
-
-        private void ensureCountInitialized(Uri uri) {
-            if (!mQueryCountMapping.containsKey(uri)) {
-                mQueryCountMapping.put(uri, 0);
-            }
-        }
-
-        private void incrementQueryCount(Uri uri) {
-            ensureCountInitialized(uri);
-            final int count = mQueryCountMapping.get(uri);
-            mQueryCountMapping.put(uri, count + 1);
-        }
     }
 }