Merge "Fix delete label a11y action not spoken." into ub-contactsdialer-h-dev
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
index 6688898..168bf22 100644
--- a/res/layout/fragment_sim_import.xml
+++ b/res/layout/fragment_sim_import.xml
@@ -54,6 +54,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="?android:colorBackground"
+            android:focusable="true"
             android:minHeight="48dp"
             android:orientation="horizontal"
             android:paddingEnd="@dimen/activity_horizontal_margin"
diff --git a/res/layout/menu_item_action_view.xml b/res/layout/menu_item_action_view.xml
new file mode 100644
index 0000000..62eb758
--- /dev/null
+++ b/res/layout/menu_item_action_view.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<!-- A dummy action view to attach extra hidden content description to menuItem for Talkback. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:visibility="gone">
+    <View
+        android:layout_width="1dp"
+        android:layout_height= "1dp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a544c08..1615cf7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -158,7 +158,7 @@
     <!-- Menu item that links an aggregate with another aggregate -->
     <string name="menu_joinAggregate">Link</string>
 
-    <!-- Menu item that opens a dialog that shows all linked contacts that make up this one. [CHAR LIMIT=30] -->
+    <!-- Menu item that opens a dialog that shows all linked contacts that make up this one. [CHAR LIMIT=35] -->
     <string name="menu_linkedContacts">View linked contacts</string>
 
     <!-- Menu item (in the action bar) to indicate that changes should be saved [CHAR LIMIT=20] -->
@@ -234,7 +234,8 @@
     <!-- Positive button text of the warning dialog contents after users select to delete a ReadOnly contact. [CHAR LIMIT=30]-->
     <string name="readOnlyContactWarning_positive_button">Hide Contact</string>
 
-    <!-- Warning dialog contents after users selects to delete a contact with ReadOnly and Writable sources. [CHAR LIMIT=NONE]-->
+    <!-- Warning dialog contents after users selects to delete a contact with ReadOnly and Writable data sources.
+         Tells the user that data from the read-only accounts will be hidden and not deleted. [CHAR LIMIT=NONE]-->
     <string name="readOnlyContactDeleteConfirmation">Read-only accounts in this contact will be hidden, not deleted.</string>
 
     <!-- Confirmation dialog. Shown after user selects to delete one writable contact [CHAR LIMIT=NONE]  -->
@@ -1883,8 +1884,8 @@
          when the window is first opened [CHAR LIMIT=40] -->
     <string name="sim_import_title">Import from SIM</string>
 
-    <!-- Content description of the cancel navigation icon shown in SIM import screen toolbar -->
-    <string name="sim_import_cancel_content_description">Cancel import</string>
+    <!-- Content description of the cancel navigation icon shown in SIM import screen toolbar [CHAR LIMIT=NONE]-->
+    <string name="sim_import_cancel_content_description">Cancel</string>
 
     <!-- Alert for letting user know that their device auto-sync setting is turned off,
          in case they are wondering why they are not seeing any contact. [CHAR LIMIT=150] -->
diff --git a/src/com/android/contacts/ContactsDrawerActivity.java b/src/com/android/contacts/ContactsDrawerActivity.java
index b786940..2e024d4 100644
--- a/src/com/android/contacts/ContactsDrawerActivity.java
+++ b/src/com/android/contacts/ContactsDrawerActivity.java
@@ -437,6 +437,8 @@
                         return true;
                     }
                 });
+
+                updateMenuContentDescription(menuItem, getString(R.string.group_name_dialog_hint));
             }
         }
 
@@ -528,7 +530,6 @@
             return;
         }
 
-
         for (int i = 0; i < accountFilterItems.size(); i++) {
             final ContactListFilter filter = accountFilterItems.get(i);
             final AccountDisplayInfo displayableAccount =
@@ -556,14 +557,8 @@
                 // Get rid of the default menu item overlay and show original account icons.
                 menuItem.getIcon().setColorFilter(Color.TRANSPARENT, PorterDuff.Mode.SRC_ATOP);
             }
-            // Create a dummy action view to attach extra hidden content description to the menuItem
-            // for Talkback. We want Talkback to read out the account type but not have it be part
-            // of the menuItem title.
-            LinearLayout view = (LinearLayout) LayoutInflater.from(this)
-                    .inflate(R.layout.account_type_info, null);
-            view.setContentDescription(displayableAccount.getTypeLabel());
-            view.setVisibility(View.VISIBLE);
-            menuItem.setActionView(view);
+
+            updateMenuContentDescription(menuItem, displayableAccount.getTypeLabel());
         }
 
         if (isAccountView()) {
@@ -571,6 +566,17 @@
         }
     }
 
+    private void updateMenuContentDescription(MenuItem menuItem, CharSequence contentDescription) {
+        // Create a dummy action view to attach extra hidden content description to the menuItem
+        // for Talkback. We want Talkback to read out the account type but not have it be part
+        // of the menuItem title.
+        final LinearLayout view = (LinearLayout) LayoutInflater.from(this)
+                .inflate(R.layout.menu_item_action_view, null);
+        view.setContentDescription(contentDescription);
+        view.setVisibility(View.VISIBLE);
+        menuItem.setActionView(view);
+    }
+
     public void updateFilterMenu(ContactListFilter filter) {
         clearCheckedMenus();
         if (filter != null && filter.isContactsFilterType()) {
diff --git a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
index 17c3392..8ce6ecb 100644
--- a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
@@ -200,7 +200,6 @@
             intent.setClass(this, ContactEditorActivity.class);
         }
         startEditorAndForwardExtras(intent);
-        finish();
     }
 
     /**
@@ -233,6 +232,7 @@
             intent.putExtras(extras);
         }
         ImplicitIntentsUtil.startActivityInApp(this, intent);
+        finish();
     }
 
     private void toastErrorAndFinish() {
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index 3175ce4..b5b6626 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -29,6 +29,8 @@
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.util.ArrayMap;
@@ -36,6 +38,7 @@
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.SparseArray;
 
 import com.android.contacts.R;
@@ -51,6 +54,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -183,6 +187,10 @@
         return null;
     }
 
+    /**
+     * Finds SIM contacts that exist in CP2 and associates the account of the CP2 contact with
+     * the SIM contact
+     */
     public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
             List<SimContact> contacts) {
         final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
@@ -238,7 +246,6 @@
         }
     }
 
-
     private ContentProviderResult[] importBatch(List<SimContact> contacts,
             AccountWithDataSet targetAccount)
             throws RemoteException, OperationApplicationException {
@@ -305,27 +312,49 @@
         final StringBuilder selectionBuilder = new StringBuilder();
 
         int phoneCount = 0;
+        int nameCount = 0;
         for (SimContact contact : contacts) {
             if (contact.hasPhone()) {
                 phoneCount++;
+            } else if (contact.hasName()) {
+                nameCount++;
             }
         }
         List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
 
-        selectionBuilder.append(ContactsContract.Data.MIMETYPE).append("=? AND ");
+        selectionBuilder.append('(');
+        selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
         selectionArgs.add(Phone.CONTENT_ITEM_TYPE);
 
         selectionBuilder.append(Phone.NUMBER).append(" IN (")
                 .append(Joiner.on(',').join(Collections.nCopies(phoneCount, '?')))
-                .append(")");
+                .append(')');
         for (SimContact contact : contacts) {
             if (contact.hasPhone()) {
                 selectionArgs.add(contact.getPhone());
             }
         }
+        selectionBuilder.append(')');
 
-        return mResolver.query(ContactsContract.Data.CONTENT_URI.buildUpon()
-                        .appendQueryParameter(ContactsContract.Data.VISIBLE_CONTACTS_ONLY, "true")
+        if (nameCount > 0) {
+            selectionBuilder.append(" OR (");
+
+            selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
+            selectionArgs.add(StructuredName.CONTENT_ITEM_TYPE);
+
+            selectionBuilder.append(Data.DISPLAY_NAME).append(" IN (")
+                    .append(Joiner.on(',').join(Collections.nCopies(nameCount, '?')))
+                    .append(')');
+            for (SimContact contact : contacts) {
+                if (!contact.hasPhone() && contact.hasName()) {
+                    selectionArgs.add(contact.getName());
+                }
+            }
+            selectionBuilder.append(')');
+        }
+
+        return mResolver.query(Data.CONTENT_URI.buildUpon()
+                        .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
                         .build(),
                 DataQuery.PROJECTION,
                 selectionBuilder.toString(),
@@ -439,24 +468,29 @@
     private static final class DataQuery {
 
         public static final String[] PROJECTION = new String[] {
-                ContactsContract.Data.RAW_CONTACT_ID, Phone.NUMBER, Phone.DISPLAY_NAME
+                Data.RAW_CONTACT_ID, Phone.NUMBER, Data.DISPLAY_NAME, Data.MIMETYPE
         };
 
         public static final int RAW_CONTACT_ID = 0;
         public static final int PHONE_NUMBER = 1;
         public static final int DISPLAY_NAME = 2;
+        public static final int MIMETYPE = 3;
 
         public static long getRawContactId(Cursor cursor) {
             return cursor.getLong(RAW_CONTACT_ID);
         }
 
         public static String getPhoneNumber(Cursor cursor) {
-            return cursor.getString(PHONE_NUMBER);
+            return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
         }
 
         public static String getDisplayName(Cursor cursor) {
             return cursor.getString(DISPLAY_NAME);
         }
+
+        public static boolean isPhoneNumber(Cursor cursor) {
+            return Phone.CONTENT_ITEM_TYPE.equals(cursor.getString(MIMETYPE));
+        }
     }
 
     private static final class AccountQuery {
diff --git a/src/com/android/contacts/common/model/ContactLoader.java b/src/com/android/contacts/common/model/ContactLoader.java
index 0c569ba..2cafc1f 100644
--- a/src/com/android/contacts/common/model/ContactLoader.java
+++ b/src/com/android/contacts/common/model/ContactLoader.java
@@ -299,8 +299,9 @@
         public static final int EXPORT_SUPPORT = 5;
     }
 
-    public void setLookupUri(Uri lookupUri) {
+    public void setNewLookup(Uri lookupUri) {
         mLookupUri = lookupUri;
+        mContact = null;
     }
 
     @Override
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
index 25af5f8..2d26029 100644
--- a/src/com/android/contacts/common/model/SimContact.java
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -48,7 +48,7 @@
     public SimContact(long id, String name, String phone, String[] emails) {
         mId = id;
         mName = name;
-        mPhone = phone;
+        mPhone = phone == null ? "" : phone.trim();
         mEmails = emails;
     }
 
@@ -70,8 +70,8 @@
 
     public void appendCreateContactOperations(List<ContentProviderOperation> ops,
             AccountWithDataSet targetAccount) {
-        // nothing to save.
-        if (mName == null && mPhone == null && mEmails == null) return;
+        // There is nothing to save so skip it.
+        if (!hasName() && !hasPhone() && !hasEmails()) return;
 
         final int rawContactOpIndex = ops.size();
         ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
@@ -84,7 +84,7 @@
             ops.add(createInsertOp(rawContactOpIndex, StructuredName.CONTENT_ITEM_TYPE,
                     StructuredName.DISPLAY_NAME, mName));
         }
-        if (mPhone != null) {
+        if (!mPhone.isEmpty()) {
             ops.add(createInsertOp(rawContactOpIndex, Phone.CONTENT_ITEM_TYPE,
                     Phone.NUMBER, mPhone));
         }
@@ -116,7 +116,7 @@
     }
 
     public boolean hasPhone() {
-        return mPhone != null;
+        return !mPhone.isEmpty();
     }
 
     public boolean hasEmails() {
@@ -230,8 +230,7 @@
             @Override
             public int compare(SimContact lhs, SimContact rhs) {
                 return ComparisonChain.start()
-                        .compare(lhs.mPhone, rhs.mPhone,
-                                Ordering.<String>natural().nullsFirst())
+                        .compare(lhs.mPhone, rhs.mPhone)
                         .compare(lhs.mName, rhs.mName, Ordering.<String>natural().nullsFirst())
                         .result();
             }
diff --git a/src/com/android/contacts/common/model/account/ExternalAccountType.java b/src/com/android/contacts/common/model/account/ExternalAccountType.java
index 6803a6b..5c5a26a 100644
--- a/src/com/android/contacts/common/model/account/ExternalAccountType.java
+++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -418,7 +418,7 @@
             return -1; // Empty text is okay.
         }
         if (resourceName.charAt(0) != '@') {
-            if (Log.isLoggable(TAG, Log.WARN)) {
+            if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
                 Log.w(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
             }
             return -1;
@@ -428,18 +428,23 @@
         try {
              res = context.getPackageManager().getResourcesForApplication(packageName);
         } catch (NameNotFoundException e) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
+            if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
                 Log.w(TAG, "Unable to load package " + packageName);
             }
             return -1;
         }
         final int resId = res.getIdentifier(name, null, packageName);
         if (resId == 0) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
+            if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
                 Log.w(TAG, "Unable to load " + resourceName + " from package " + packageName);
             }
             return -1;
         }
         return resId;
     }
+
+    @VisibleForTesting
+    static boolean isFromTestApp(String packageName) {
+        return TextUtils.equals(packageName, "com.google.android.contacts.tests");
+    }
 }
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
index 440df94..61a440f 100644
--- a/src/com/android/contacts/editor/AccountHeaderPresenter.java
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -189,6 +189,9 @@
                 UiClosables.closeQuietly(popup);
                 final AccountWithDataSet newAccount = adapter.getItem(position);
                 setCurrentAccount(newAccount);
+                // Make sure the new selection will be announced once it's changed
+                mAccountHeaderContainer.setAccessibilityLiveRegion(
+                        View.ACCESSIBILITY_LIVE_REGION_POLITE);
             }
         });
         mAccountHeaderContainer.post(new Runnable() {
diff --git a/src/com/android/contacts/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
index ecd963b..42776ff 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
@@ -207,10 +207,6 @@
         appendValue(nameSb, values, StructuredName.FAMILY_NAME);
         appendValue(nameSb, values, StructuredName.SUFFIX);
 
-        if (nameSb.length() == 0) {
-            appendValue(nameSb, values, StructuredName.DISPLAY_NAME);
-        }
-
         StringBuilder phoneticNameSb = new StringBuilder();
         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME);
         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME);
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index a98baad..81a3741 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -1015,7 +1015,7 @@
             destroyInteractionLoaders();
             mContactLoader = (ContactLoader) (Loader<?>) getLoaderManager().getLoader(
                     LOADER_CONTACT_ID);
-            mContactLoader.setLookupUri(mLookupUri);
+            mContactLoader.setNewLookup(mLookupUri);
             mCachedCp2DataCardModel = null;
         }
         mContactLoader.forceLoad();
diff --git a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
index 265600c..ec03f0f 100644
--- a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -25,6 +25,7 @@
 import android.support.annotation.RequiresApi;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.Suppress;
 import android.support.test.runner.AndroidJUnit4;
@@ -45,10 +46,14 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static android.os.Build.VERSION_CODES;
 import static org.hamcrest.Matchers.allOf;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(Enclosed.class)
 public class SimContactDaoTests {
@@ -197,6 +202,54 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = VERSION_CODES.M)
+    // Lollipop MR1 is required for removeAccountExplicitly
+    @RequiresApi(api = VERSION_CODES.LOLLIPOP_MR1)
+    @MediumTest
+    @RunWith(AndroidJUnit4.class)
+    public static class ExistingContactsTest {
+
+        private Context mContext;
+        private AccountsTestHelper mAccountHelper;
+        private AccountWithDataSet mAccount;
+        // We need to generate something distinct to prevent flakiness on devices that may not
+        // start with an empty CP2 DB
+        private String mNameSuffix = "";
+
+        @Before
+        public void setUp() {
+            mContext = InstrumentationRegistry.getTargetContext();
+            mAccountHelper = new AccountsTestHelper(InstrumentationRegistry.getContext());
+            mAccount = mAccountHelper.addTestAccount();
+            mNameSuffix = "testAt" + System.nanoTime();
+        }
+
+        @After
+        public void tearDown() {
+            mAccountHelper.cleanup();
+        }
+
+        @Test
+        public void findAccountsOfExistingContactsReturnsEmptyMapWhenNoMatchingContactsExist() {
+            final SimContactDao sut = createDao();
+
+            final List<SimContact> contacts = Arrays.asList(
+                    new SimContact(1, "Name 1 " + mNameSuffix, "15095550101", null),
+                    new SimContact(2, "Name 2 " + mNameSuffix, "15095550102", null),
+                    new SimContact(3, "Name 3 " + mNameSuffix, "15095550103", null),
+                    new SimContact(4, "Name 4 " + mNameSuffix, "15095550104", null));
+
+            final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+                    .findAccountsOfExistingSimContacts(contacts);
+
+            assertTrue(existing.isEmpty());
+        }
+
+        private SimContactDao createDao() {
+            return SimContactDao.create(mContext);
+        }
+    }
+
     @LargeTest
     // suppressed because failed assumptions are reported as test failures by the build server
     @Suppress
diff --git a/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
index 96e776f..0ddeb67 100644
--- a/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
+++ b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
@@ -79,6 +79,11 @@
                 "@string/test_string", packageName, ""));
     }
 
+    public void testIsFromTestApp() {
+        assertTrue(ExternalAccountType.isFromTestApp("com.google.android.contacts.tests"));
+        assertFalse(ExternalAccountType.isFromTestApp("com.google.android.contacts"));
+    }
+
     /**
      * Initialize with an invalid package name and see if type will be initialized, but empty.
      */