Merge "Import translations. DO NOT MERGE" into ub-contactsdialer-h-dev
diff --git a/res/color/contact_list_name_text_color.xml b/res/color/contact_list_name_text_color.xml
new file mode 100644
index 0000000..2327e2a
--- /dev/null
+++ b/res/color/contact_list_name_text_color.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/disabled_contact_list_name_text" android:state_enabled="false" />
+    <item android:color="@color/primary_text_color" />
+</selector>
\ No newline at end of file
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
index 6b8c3fe..95864c8 100644
--- a/res/layout/fragment_sim_import.xml
+++ b/res/layout/fragment_sim_import.xml
@@ -13,69 +13,76 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="match_parent">
 
-    <android.support.v7.widget.Toolbar
-        android:id="@+id/toolbar"
+    <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize"
-        android:background="?attr/colorPrimary"
-        android:elevation="4dp"
-        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
-        app:navigationContentDescription="@string/sim_import_cancel_content_description"
-        app:navigationIcon="@drawable/ic_close_dk"
-        app:title="@string/sim_import_title_none_selected">
+        android:layout_height="match_parent"
+        android:orientation="vertical">
 
-        <Button
-            android:id="@+id/import_button"
-            style="@style/Widget.AppCompat.Button.Borderless"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="right|center_vertical"
-            android:text="@string/sim_import_button_text"
-            />
-    </android.support.v7.widget.Toolbar>
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginBottom="16dp"
-        android:background="?android:colorBackground"
-        android:elevation="4dp">
-
-        <include layout="@layout/editor_account_header"/>
-    </FrameLayout>
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <ListView
-            android:id="@+id/list"
+        <android.support.v7.widget.Toolbar
+            android:id="@+id/toolbar"
+            style="@style/ContactsToolbarStyle"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
+            android:layout_height="?attr/actionBarSize"
+            android:background="?attr/colorPrimary"
+            android:elevation="4dp"
+            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+            app:navigationContentDescription="@string/sim_import_cancel_content_description"
+            app:navigationIcon="@drawable/ic_close_dk"
+            app:title="@string/sim_import_title_none_selected">
 
-        <android.support.v4.widget.ContentLoadingProgressBar
-            android:id="@+id/loading_progress"
-            style="@style/Widget.AppCompat.ProgressBar"
-            android:layout_width="wrap_content"
+            <Button
+                android:id="@+id/import_button"
+                style="@style/Widget.AppCompat.Button.Borderless"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="right|center_vertical"
+                android:text="@string/sim_import_button_text"
+                />
+        </android.support.v7.widget.Toolbar>
+
+        <FrameLayout
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:indeterminate="true"/>
+            android:layout_marginBottom="16dp"
+            android:background="?android:colorBackground"
+            android:elevation="4dp">
 
-        <TextView
-            android:id="@+id/empty_message"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:text="@string/sim_import_empty_message"
-            android:textAppearance="?android:textAppearanceMedium"
-            android:visibility="gone"/>
+            <include layout="@layout/editor_account_header"/>
+        </FrameLayout>
 
-    </FrameLayout>
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
 
-</LinearLayout>
\ No newline at end of file
+            <ListView
+                android:id="@+id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"/>
+
+            <android.support.v4.widget.ContentLoadingProgressBar
+                android:id="@+id/loading_progress"
+                style="@style/Widget.AppCompat.ProgressBar"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:indeterminate="true"/>
+
+            <TextView
+                android:id="@+id/empty_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:text="@string/sim_import_empty_message"
+                android:textAppearance="?android:textAppearanceMedium"
+                android:visibility="gone"/>
+
+        </FrameLayout>
+
+    </LinearLayout>
+</android.support.design.widget.CoordinatorLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 6dbe010..03ca621 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -83,6 +83,9 @@
     <!-- Color of background of disabled link contacts button, 15% black. -->
     <color name="disabled_button_background">#26000000</color>
 
+    <!-- Color of text of people names in contact list when item is disabled. 30% black -->
+    <color name="disabled_contact_list_name_text">#4D000000</color>
+
     <!-- Color of background of all empty states. -->
     <color name="empty_state_background">#efefef</color>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 015d612..36315a2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1818,6 +1818,10 @@
          SIM card  -->
     <string name="sim_import_empty_message">No contacts on your SIM card</string>
 
+    <!-- Snackbar message shown on the SIM import screen when the user taps on a contact that
+         is disabled because an exact duplicate already exists in their contacts [CHAR LIMIT=60] -->
+    <string name="sim_import_contact_exists_toast">Contact already exists in your list</string>
+
     <!-- Toast shown on settings screen when importing from SIM completes successfully -->
     <plurals name="sim_import_success_toast_fmt">
         <item quantity="one">1 SIM contact imported</item>
@@ -1857,4 +1861,7 @@
 
     <!-- No network connection error message [CHAR LIMIT=50] -->
     <string name="connection_error_message">No connection</string>
+
+    <!-- Generic label for the SIM card when there is only a single SIM in the device [CHAR LIMIT=10]-->
+    <string name="single_sim_display_label">SIM</string>
 </resources>
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index ed043bf..bc20d62 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -25,6 +25,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
 import android.support.v4.widget.ContentLoadingProgressBar;
 import android.support.v7.widget.Toolbar;
 import android.view.LayoutInflater;
@@ -132,7 +133,12 @@
         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                mAdapter.toggleSelectionOfContactId(id);
+                if (mAdapter.existsInContacts(position)) {
+                    Snackbar.make(getView(), R.string.sim_import_contact_exists_toast,
+                            Snackbar.LENGTH_LONG).show();
+                } else {
+                    mAdapter.toggleSelectionOfContactId(id);
+                }
             }
         });
         mImportButton = view.findViewById(R.id.import_button);
@@ -185,14 +191,17 @@
     @Override
     public void onLoadFinished(Loader<ArrayList<SimContact>> loader,
             ArrayList<SimContact> data) {
+        mLoadingIndicator.hide();
         mListView.setEmptyView(getView().findViewById(R.id.empty_message));
+        if (data == null) {
+            return;
+        }
         mAdapter.setContacts(data);
         if (mSelectedContacts != null) {
             mAdapter.select(mSelectedContacts);
         } else {
             mAdapter.selectAll();
         }
-        mLoadingIndicator.hide();
     }
 
     @Override
@@ -258,6 +267,7 @@
 
     private static class SimContactAdapter extends ContactListAdapter {
         private ArrayList<SimContact> mContacts;
+        private static float DISABLED_AVATAR_ALPHA = 0.38f;
 
         public SimContactAdapter(Context context) {
             super(context);
@@ -278,6 +288,7 @@
             // clickable
             contactView.getCheckBox().setFocusable(false);
             contactView.getCheckBox().setClickable(false);
+            setViewEnabled(contactView, !mContacts.get(cursor.getPosition()).existsInContacts());
         }
 
         public void setContacts(ArrayList<SimContact> contacts) {
@@ -304,7 +315,9 @@
 
             final TreeSet<Long> selected = new TreeSet<>();
             for (SimContact contact : mContacts) {
-                selected.add(contact.getId());
+                if (!contact.existsInContacts()) {
+                    selected.add(contact.getId());
+                }
             }
             setSelectedContactIds(selected);
         }
@@ -316,6 +329,16 @@
             }
             setSelectedContactIds(selected);
         }
+
+        public boolean existsInContacts(int position) {
+            return mContacts.get(position).existsInContacts();
+        }
+
+        private void setViewEnabled(ContactListItemView itemView, boolean enabled) {
+            itemView.getCheckBox().setEnabled(enabled);
+            itemView.getPhotoView().setAlpha(enabled ? 1f : DISABLED_AVATAR_ALPHA);
+            itemView.getNameTextView().setEnabled(enabled);
+        }
     }
 
     public static class SimContactLoader extends AsyncTaskLoader<ArrayList<SimContact>> {
@@ -346,11 +369,11 @@
 
         @Override
         public ArrayList<SimContact> loadInBackground() {
-            if (mSubscriptionId != SimCard.NO_SUBSCRIPTION_ID) {
-                return mDao.loadSimContacts(mSubscriptionId);
-            } else {
-                return mDao.loadSimContacts();
+            final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId);
+            if (sim == null) {
+                return new ArrayList<>();
             }
+            return mDao.loadSimContactsWithExistingContactIds(sim);
         }
 
         @Override
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 513109c..fd6ee89 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -272,7 +272,8 @@
 
     @Override
     protected void onNewIntent(Intent intent) {
-        if (GroupUtil.ACTION_CREATE_GROUP.equals(intent.getAction())) {
+        final String action = intent.getAction();
+        if (GroupUtil.ACTION_CREATE_GROUP.equals(action)) {
             mGroupUri = intent.getData();
             if (mGroupUri == null) {
                 toast(R.string.groupSavedErrorToast);
@@ -280,19 +281,19 @@
             }
             if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Received group URI " + mGroupUri);
             switchView(ContactsView.GROUP_VIEW);
-            mMembersFragment.toastForSaveAction(intent.getAction());
+            mMembersFragment.toastForSaveAction(action);
             return;
         }
 
-        if (isGroupDeleteAction(intent.getAction())) {
+        if (isGroupDeleteAction(action)) {
             popSecondLevel();
-            mMembersFragment.toastForSaveAction(intent.getAction());
+            mMembersFragment.toastForSaveAction(action);
             mCurrentView = ContactsView.ALL_CONTACTS;
             showFabWithAnimation(/* showFab */ true);
             return;
         }
 
-        if (isGroupSaveAction(intent.getAction())) {
+        if (isGroupSaveAction(action)) {
             mGroupUri = intent.getData();
             if (mGroupUri == null) {
                 popSecondLevel();
@@ -300,8 +301,15 @@
                 return;
             }
             if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Received group URI " + mGroupUri);
-            switchView(ContactsView.GROUP_VIEW);
-            mMembersFragment.toastForSaveAction(intent.getAction());
+            // ACTION_REMOVE_FROM_GROUP doesn't reload data, so it shouldn't cause b/32223934
+            // but it's necessary to use the previous fragment since
+            // GroupMembersFragment#mIsEditMode needs to be persisted between remove actions.
+            if (GroupUtil.ACTION_REMOVE_FROM_GROUP.equals(action)) {
+                switchToOrUpdateGroupView(action);
+            } else {
+                switchView(ContactsView.GROUP_VIEW);
+            }
+            mMembersFragment.toastForSaveAction(action);
         }
 
         setIntent(intent);
@@ -747,7 +755,7 @@
         mGroupUri = savedInstanceState.getParcelable(KEY_GROUP_URI);
     }
 
-    private void onGroupDeleted(Intent intent) {
+    private void onGroupDeleted(final Intent intent) {
         if (!ContactSaveService.canUndo(intent)) return;
 
         Snackbar.make(mLayoutRoot, getString(R.string.groupDeletedToast), Snackbar.LENGTH_LONG)
@@ -822,7 +830,7 @@
             mMembersFragment = GroupMembersFragment.newInstance(mGroupUri);
             transaction.replace(
                     R.id.contacts_list_container, mMembersFragment, TAG_GROUP_VIEW);
-        } else if(isAssistantView()) {
+        } else if (isAssistantView()) {
             String fragmentTag;
             if (Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
                 fragmentTag = TAG_ASSISTANT;
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index f5880a8..cab2906 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -29,13 +29,14 @@
 import android.os.RemoteException;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
-import android.support.annotation.NonNull;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.annotation.VisibleForTesting;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.SparseArray;
 
+import com.android.contacts.R;
 import com.android.contacts.common.Experiments;
 import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.model.SimCard;
@@ -44,8 +45,10 @@
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.util.SharedPreferenceUtil;
 import com.android.contactsbind.experiments.Flags;
+import com.google.common.base.Joiner;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -56,6 +59,12 @@
 public class SimContactDao {
     private static final String TAG = "SimContactDao";
 
+    // Maximum number of SIM contacts to import in a single ContentResolver.applyBatch call.
+    // This is necessary to avoid TransactionTooLargeException when there are a large number of
+    // contacts. This has been tested on Nexus 6 NME70B and is probably be conservative enough
+    // to work on any phone.
+    private static final int IMPORT_MAX_BATCH_SIZE = 300;
+
     // Set to true for manual testing on an emulator or phone without a SIM card
     // DO NOT SUBMIT if set to true
     private static final boolean USE_FAKE_INSTANCE = false;
@@ -72,7 +81,7 @@
     private final ContentResolver mResolver;
     private final TelephonyManager mTelephonyManager;
 
-    public SimContactDao(Context context) {
+    private SimContactDao(Context context) {
         mContext = context;
         mResolver = context.getContentResolver();
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -86,20 +95,21 @@
         new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
-                for (SimCard card : getSimCards()) {
-                    // We don't actually have to do any caching ourselves. Some other layer must do
-                    // caching of the data (OS or framework) because subsequent queries are very
-                    // fast.
-                    card.loadContacts(SimContactDao.this);
-                }
+                getSimCardsWithContacts();
                 return null;
             }
         }.execute();
     }
 
+    public Context getContext() {
+        return mContext;
+    }
+
     public boolean canReadSimContacts() {
+        // Require SIM_STATE_READY because the TelephonyManager methods related to SIM require
+        // this state
         return hasTelephony() && hasPermissions() &&
-                mTelephonyManager.getSimState() != TelephonyManager.SIM_STATE_ABSENT;
+                mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY;
     }
 
     public List<SimCard> getSimCards() {
@@ -108,24 +118,26 @@
         }
         final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
                 getSimCardsFromSubscriptions() :
-                Collections.singletonList(SimCard.create(mTelephonyManager));
+                Collections.singletonList(SimCard.create(mTelephonyManager,
+                        mContext.getString(R.string.single_sim_display_label)));
         return SharedPreferenceUtil.restoreSimStates(mContext, sims);
     }
 
-    @NonNull
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
-    private List<SimCard> getSimCardsFromSubscriptions() {
-        final SubscriptionManager subscriptionManager = (SubscriptionManager)
-                mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
-        final List<SubscriptionInfo> subscriptions = subscriptionManager
-                .getActiveSubscriptionInfoList();
-        final ArrayList<SimCard> result = new ArrayList<>();
-        for (SubscriptionInfo subscriptionInfo : subscriptions) {
-            result.add(SimCard.create(subscriptionInfo));
+    public List<SimCard> getSimCardsWithContacts() {
+        final List<SimCard> result = new ArrayList<>();
+        for (SimCard sim : getSimCards()) {
+            result.add(sim.withContacts(loadContactsForSim(sim)));
         }
         return result;
     }
 
+    public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
+        if (sim.hasValidSubscriptionId()) {
+            return loadSimContacts(sim.getSubscriptionId());
+        }
+        return loadSimContacts();
+    }
+
     public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
         return loadFrom(ICC_CONTENT_URI.buildUpon()
                 .appendPath("subId")
@@ -137,8 +149,77 @@
         return loadFrom(ICC_CONTENT_URI);
     }
 
-    public Context getContext() {
-        return mContext;
+    public ArrayList<SimContact> loadSimContactsWithExistingContactIds(SimCard sim) {
+        return getSimContactsWithRawContacts(sim);
+    }
+
+    public ContentProviderResult[] importContacts(List<SimContact> contacts,
+            AccountWithDataSet targetAccount)
+            throws RemoteException, OperationApplicationException {
+        if (contacts.size() < IMPORT_MAX_BATCH_SIZE) {
+            return importBatch(contacts, targetAccount);
+        }
+        final List<ContentProviderResult> results = new ArrayList<>();
+        for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
+            results.addAll(Arrays.asList(importBatch(
+                    contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
+                    targetAccount)));
+        }
+        return results.toArray(new ContentProviderResult[results.size()]);
+    }
+
+    public void persistSimState(SimCard sim) {
+        SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
+    }
+
+    public void persistSimStates(List<SimCard> simCards) {
+        SharedPreferenceUtil.persistSimStates(mContext, simCards);
+    }
+
+    public SimCard getFirstSimCard() {
+        return getSimBySubscriptionId(SimCard.NO_SUBSCRIPTION_ID);
+    }
+
+    public SimCard getSimBySubscriptionId(int subscriptionId) {
+        final List<SimCard> sims = getSimCards();
+        if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
+            return sims.get(0);
+        }
+        for (SimCard sim : getSimCards()) {
+            if (sim.getSubscriptionId() == subscriptionId) {
+                return sim;
+            }
+        }
+        return null;
+    }
+
+    private ContentProviderResult[] importBatch(List<SimContact> contacts,
+            AccountWithDataSet targetAccount)
+            throws RemoteException, OperationApplicationException {
+        final ArrayList<ContentProviderOperation> ops =
+                createImportOperations(contacts, targetAccount);
+        return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+    private List<SimCard> getSimCardsFromSubscriptions() {
+        final SubscriptionManager subscriptionManager = (SubscriptionManager)
+                mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        final List<SubscriptionInfo> subscriptions = subscriptionManager
+                .getActiveSubscriptionInfoList();
+        if (subscriptions == null) {
+            return Collections.emptyList();
+        }
+        final ArrayList<SimCard> result = new ArrayList<>();
+        for (SubscriptionInfo subscriptionInfo : subscriptions) {
+            result.add(SimCard.create(subscriptionInfo));
+        }
+        return result;
+    }
+
+    private List<SimContact> getContactsForSim(SimCard sim) {
+        final List<SimContact> contacts = sim.getContacts();
+        return contacts != null ? contacts : loadContactsForSim(sim);
     }
 
     private ArrayList<SimContact> loadFrom(Uri uri) {
@@ -171,37 +252,82 @@
         return result;
     }
 
-    public ContentProviderResult[] importContacts(List<SimContact> contacts,
-            AccountWithDataSet targetAccount)
-            throws RemoteException, OperationApplicationException {
-        final ArrayList<ContentProviderOperation> ops =
-                createImportOperations(contacts, targetAccount);
-        return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
-    }
-
-    public void persistSimState(SimCard sim) {
-        SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
-    }
-
-    public void persistSimStates(List<SimCard> simCards) {
-        SharedPreferenceUtil.persistSimStates(mContext, simCards);
-    }
-
-    public SimCard getFirstSimCard() {
-        return getSimBySubscriptionId(SimCard.NO_SUBSCRIPTION_ID);
-    }
-
-    public SimCard getSimBySubscriptionId(int subscriptionId) {
-        final List<SimCard> sims = getSimCards();
-        if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
-            return sims.get(0);
+    private ArrayList<SimContact> getSimContactsWithRawContacts(SimCard sim) {
+        final ArrayList<SimContact> contacts = new ArrayList<>(getContactsForSim(sim));
+        for (int i = 0; i < contacts.size(); i += DataQuery.MAX_BATCH_SIZE) {
+            final List<SimContact> batch =
+                    contacts.subList(i, Math.min(i + DataQuery.MAX_BATCH_SIZE, contacts.size()));
+            setRawContactsForSimContacts(batch);
         }
-        for (SimCard sim : getSimCards()) {
-            if (sim.getSubscriptionId() == subscriptionId) {
-                return sim;
+        // Restore default sort order
+        Collections.sort(contacts, SimContact.compareById());
+        return contacts;
+    }
+
+    private void setRawContactsForSimContacts(List<SimContact> contacts) {
+        final StringBuilder selectionBuilder = new StringBuilder();
+
+        int phoneCount = 0;
+        for (SimContact contact : contacts) {
+            if (contact.hasPhone()) {
+                phoneCount++;
             }
         }
-        return null;
+        List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
+
+        selectionBuilder.append(ContactsContract.Data.MIMETYPE).append("=? AND ");
+        selectionArgs.add(Phone.CONTENT_ITEM_TYPE);
+
+        selectionBuilder.append(Phone.NUMBER).append(" IN (")
+                .append(Joiner.on(',').join(Collections.nCopies(phoneCount, '?')))
+                .append(")");
+        for (SimContact contact : contacts) {
+            if (contact.hasPhone()) {
+                selectionArgs.add(contact.getPhone());
+            }
+        }
+
+        final Cursor cursor = mResolver.query(ContactsContract.Data.CONTENT_URI.buildUpon()
+                        .appendQueryParameter(ContactsContract.Data.VISIBLE_CONTACTS_ONLY, "true")
+                        .build(),
+                DataQuery.PROJECTION,
+                selectionBuilder.toString(),
+                selectionArgs.toArray(new String[selectionArgs.size()]),
+                ContactsContract.Data.RAW_CONTACT_ID + " ASC");
+
+        if (cursor == null) {
+            initializeRawContactIds(contacts);
+            return;
+        }
+
+        try {
+            setRawContactsForSimContacts(contacts, cursor);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void initializeRawContactIds(List<SimContact> contacts) {
+        for (int i = 0; i < contacts.size(); i++) {
+            contacts.set(i, contacts.get(i).withRawContactId(SimContact.NO_EXISTING_CONTACT));
+        }
+    }
+
+    private void setRawContactsForSimContacts(List<SimContact> contacts, Cursor cursor) {
+        initializeRawContactIds(contacts);
+        Collections.sort(contacts, SimContact.compareByPhoneThenName());
+
+        while (cursor.moveToNext()) {
+            final String number = DataQuery.getPhoneNumber(cursor);
+            final String name = DataQuery.getDisplayName(cursor);
+
+            int index = SimContact.findByPhoneAndName(contacts, number, name);
+            if (index < 0) {
+                continue;
+            }
+            final SimContact contact = contacts.get(index);
+            contacts.set(index, contact.withRawContactId(DataQuery.getRawContactId(cursor)));
+        }
     }
 
     private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
@@ -242,10 +368,10 @@
                             new SimContact(1, "John Sim", "15095550121", null),
                             new SimContact(2, "Bob Sim", "15095550122", null),
                             new SimContact(3, "Mary Sim", "15095550123", null),
-                            new SimContact(4, "Alice Sim", "15095550124", null)
+                            new SimContact(4, "Alice Sim", "15095550124", null),
+                            new SimContact(5, "Sim Duplicate", "15095550121", null)
                     ));
         }
-
         return new SimContactDao(context);
     }
 
@@ -290,4 +416,31 @@
             return true;
         }
     }
+
+    // Query used for detecting existing contacts that may match a SimContact.
+    private static final class DataQuery {
+        // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+        // query parameter limit.
+        static final int MAX_BATCH_SIZE = 100;
+
+        public static final String[] PROJECTION = new String[] {
+                ContactsContract.Data.RAW_CONTACT_ID, Phone.NUMBER, Phone.DISPLAY_NAME
+        };
+
+        public static final int RAW_CONTACT_ID = 0;
+        public static final int PHONE_NUMBER = 1;
+        public static final int DISPLAY_NAME = 2;
+
+        public static long getRawContactId(Cursor cursor) {
+            return cursor.getLong(RAW_CONTACT_ID);
+        }
+
+        public static String getPhoneNumber(Cursor cursor) {
+            return cursor.getString(PHONE_NUMBER);
+        }
+
+        public static String getDisplayName(Cursor cursor) {
+            return cursor.getString(DISPLAY_NAME);
+        }
+    }
 }
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index f74c671..6e742a3 100644
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -31,6 +31,7 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.SearchSnippets;
 import android.support.v4.content.ContextCompat;
+import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.graphics.drawable.DrawableCompat;
 import android.support.v7.widget.AppCompatCheckBox;
 import android.support.v7.widget.AppCompatImageButton;
@@ -1156,7 +1157,8 @@
             mNameTextView = new TextView(getContext());
             mNameTextView.setSingleLine(true);
             mNameTextView.setEllipsize(getTextEllipsis());
-            mNameTextView.setTextColor(mNameTextViewTextColor);
+            mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(),
+                    R.color.contact_list_name_text_color, getContext().getTheme()));
             mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
             // Manually call setActivated() since this view may be added after the first
             // setActivated() call toward this whole item view.
diff --git a/src/com/android/contacts/common/model/SimCard.java b/src/com/android/contacts/common/model/SimCard.java
index 9b5486b..3c27cfe 100644
--- a/src/com/android/contacts/common/model/SimCard.java
+++ b/src/com/android/contacts/common/model/SimCard.java
@@ -22,10 +22,8 @@
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
-import com.android.contacts.common.database.SimContactDao;
-
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
@@ -65,7 +63,9 @@
         mCountryCode = other.mCountryCode;
         mDismissed = other.mDismissed;
         mImported = other.mImported;
-        mContacts = other.mContacts;
+        if (other.mContacts != null) {
+            mContacts = new ArrayList<>(other.mContacts);
+        }
     }
 
     public SimCard(String simId, int subscriptionId, CharSequence carrierName,
@@ -91,6 +91,10 @@
         return mSubscriptionId;
     }
 
+    public boolean hasValidSubscriptionId() {
+        return mSubscriptionId != NO_SUBSCRIPTION_ID;
+    }
+
     public CharSequence getDisplayName() {
         return mDisplayName;
     }
@@ -106,6 +110,10 @@
         return PhoneNumberUtils.formatNumber(mPhoneNumber, mCountryCode);
     }
 
+    public String getCountryCode() {
+        return mCountryCode;
+    }
+
     public boolean hasContacts() {
         if (mContacts == null) {
             throw new IllegalStateException("Contacts not loaded.");
@@ -137,24 +145,9 @@
     }
 
     public List<SimContact> getContacts() {
-        if (mContacts == null) {
-            throw new IllegalStateException("Contacts not loaded.");
-        }
         return mContacts;
     }
 
-    public synchronized void loadContacts(SimContactDao dao) {
-        if (mSubscriptionId == NO_SUBSCRIPTION_ID) {
-            // Load from the default SIM card.
-            mContacts = dao.loadSimContacts();
-        } else {
-            mContacts = dao.loadSimContacts(mSubscriptionId);
-        }
-        if (mContacts == null) {
-            mContacts = Collections.emptyList();
-        }
-    }
-
     public SimCard withImportAndDismissStates(boolean imported, boolean dismissed) {
         SimCard copy = new SimCard(this);
         copy.mImported = imported;
@@ -170,6 +163,12 @@
         return withImportAndDismissStates(dismissed, mImported);
     }
 
+    public SimCard withContacts(List<SimContact> contacts) {
+        final SimCard copy = new SimCard(this);
+        copy.mContacts = contacts;
+        return copy;
+    }
+
     public SimCard withContacts(SimContact... contacts) {
         final SimCard copy = new SimCard(this);
         copy.mContacts = Arrays.asList(contacts);
@@ -206,10 +205,15 @@
                 info.getCountryIso());
     }
 
-    public static SimCard create(TelephonyManager telephony) {
-        SimCard simCard = new SimCard(telephony.getSimSerialNumber(),
-                telephony.getNetworkOperatorName(), "SIM", telephony.getLine1Number(),
-                telephony.getNetworkCountryIso());
-        return simCard;
+    public static SimCard create(TelephonyManager telephony, String displayLabel) {
+        if (telephony.getSimState() == TelephonyManager.SIM_STATE_READY) {
+            return new SimCard(telephony.getSimSerialNumber(),
+                    telephony.getSimOperatorName(), displayLabel, telephony.getLine1Number(),
+                    telephony.getSimCountryIso());
+        } else {
+            // This should never happen but in case it does just fallback to an "empty" instance
+            return new SimCard(/* SIM id */ "", /* operator name */ null, displayLabel,
+                    /* phone number */ "", /* Country code */ null);
+        }
     }
 }
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
index a13ef67..3768503 100644
--- a/src/com/android/contacts/common/model/SimContact.java
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -26,27 +26,41 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 
 import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Holds data for contacts loaded from the SIM card.
  */
 public class SimContact implements Parcelable {
+    public static final long EXISTING_CONTACT_UNINITIALIZED = -2;
+    public static final long NO_EXISTING_CONTACT = -1;
+
     private final long mId;
     private final String mName;
     private final String mPhone;
     private final String[] mEmails;
 
+    private final long mRawContactId;
+
     public SimContact(long id, String name, String phone, String[] emails) {
-        this.mId = id;
-        this.mName = name;
-        this.mPhone = phone;
-        this.mEmails = emails;
+        this(id, name, phone, emails, EXISTING_CONTACT_UNINITIALIZED);
     }
 
+    public SimContact(long id, String name, String phone, String[] emails, long rawContactId) {
+        mId = id;
+        mName = name;
+        mPhone = phone;
+        mEmails = emails;
+        mRawContactId = rawContactId;
+    }
     public long getId() {
         return mId;
     }
@@ -63,6 +77,13 @@
         return mEmails;
     }
 
+    public boolean existsInContacts() {
+        if (mRawContactId == EXISTING_CONTACT_UNINITIALIZED) {
+            throw new IllegalStateException("Raw contact ID is uninitialized");
+        }
+        return mRawContactId > 0;
+    }
+
     public void appendCreateContactOperations(List<ContentProviderOperation> ops,
             AccountWithDataSet targetAccount) {
         // nothing to save.
@@ -106,6 +127,22 @@
                 .add(ContactsContract.Contacts.LOOKUP_KEY, getLookupKey());
     }
 
+    public boolean hasName() {
+        return mName != null;
+    }
+
+    public boolean hasPhone() {
+        return mPhone != null;
+    }
+
+    public boolean hasEmails() {
+        return mEmails != null && mEmails.length > 0;
+    }
+
+    public SimContact withRawContactId(long id) {
+        return new SimContact(mId, mName, mPhone, mEmails, id);
+    }
+
     /**
      * Generate a "fake" lookup key. This is needed because
      * {@link com.android.contacts.common.ContactPhotoManager} will only generate a letter avatar
@@ -128,6 +165,7 @@
                 ", mName='" + mName + '\'' +
                 ", mPhone='" + mPhone + '\'' +
                 ", mEmails=" + Arrays.toString(mEmails) +
+                ", mRawContactId=" + mRawContactId +
                 '}';
     }
 
@@ -138,10 +176,9 @@
 
         final SimContact that = (SimContact) o;
 
-        if (mId != that.mId) return false;
-        if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
-        if (mPhone != null ? !mPhone.equals(that.mPhone) : that.mPhone != null) return false;
-        return Arrays.equals(mEmails, that.mEmails);
+        return mId == that.mId && mRawContactId == that.mRawContactId
+                && Objects.equals(mName, that.mName) && Objects.equals(mPhone, that.mPhone)
+                && Arrays.equals(mEmails, that.mEmails);
     }
 
     @Override
@@ -150,6 +187,7 @@
         result = 31 * result + (mName != null ? mName.hashCode() : 0);
         result = 31 * result + (mPhone != null ? mPhone.hashCode() : 0);
         result = 31 * result + Arrays.hashCode(mEmails);
+        result = 31 * result + (int) (mRawContactId ^ (mRawContactId >>> 32));
         return result;
     }
 
@@ -164,6 +202,7 @@
         dest.writeString(mName);
         dest.writeString(mPhone);
         dest.writeStringArray(mEmails);
+        dest.writeLong(mRawContactId);
     }
 
     /**
@@ -185,11 +224,12 @@
     public static final Creator<SimContact> CREATOR = new Creator<SimContact>() {
         @Override
         public SimContact createFromParcel(Parcel source) {
-            long id = source.readLong();
-            String name = source.readString();
-            String phone = source.readString();
-            String[] emails = source.createStringArray();
-            return new SimContact(id, name, phone, emails);
+            final long id = source.readLong();
+            final String name = source.readString();
+            final String phone = source.readString();
+            final String[] emails = source.createStringArray();
+            final long contactId = source.readLong();
+            return new SimContact(id, name, phone, emails, contactId);
         }
 
         @Override
@@ -197,4 +237,39 @@
             return new SimContact[size];
         }
     };
+
+    /**
+     * Returns the index of a contact with a matching name and phone
+     * @param contacts list to search. Should be sorted using
+     * {@link SimContact#compareByPhoneThenName()}
+     * @param phone the phone to search for
+     * @param name the name to search for
+     */
+    public static int findByPhoneAndName(List<SimContact> contacts, String phone, String name) {
+        return Collections.binarySearch(contacts, new SimContact(-1, name, phone, null,
+                NO_EXISTING_CONTACT), compareByPhoneThenName());
+    }
+
+    public static final Comparator<SimContact> compareByPhoneThenName() {
+        return new Comparator<SimContact>() {
+            @Override
+            public int compare(SimContact lhs, SimContact rhs) {
+                return ComparisonChain.start()
+                        .compare(lhs.mPhone, rhs.mPhone,
+                                Ordering.<String>natural().nullsFirst())
+                        .compare(lhs.mName, rhs.mName, Ordering.<String>natural().nullsFirst())
+                        .result();
+            }
+        };
+    }
+
+    public static final Comparator<SimContact> compareById() {
+        return new Comparator<SimContact>() {
+            @Override
+            public int compare(SimContact lhs, SimContact rhs) {
+                // We assume ids are unique.
+                return Long.compare(lhs.mId, rhs.mId);
+            }
+        };
+    }
 }
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index 1b9818f..901a15e 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -268,12 +268,13 @@
             getPreferenceScreen().removePreference(findPreference(KEY_DISPLAY_ORDER));
         }
 
-        // Remove the "Default account" setting if there aren't any writable accounts
+        // Remove the default account and custom view settings there aren't any writable accounts
         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
         final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(
                 /* contactWritableOnly */ true);
         if (accounts.isEmpty()) {
             getPreferenceScreen().removePreference(findPreference(KEY_DEFAULT_ACCOUNT));
+            getPreferenceScreen().removePreference(findPreference(KEY_CUSTOM_CONTACTS_FILTER));
         }
 
         final boolean isPhone = TelephonyManagerCompat.isVoiceCapable(
diff --git a/src/com/android/contacts/group/GroupMembersFragment.java b/src/com/android/contacts/group/GroupMembersFragment.java
index 4f8f6a8..e71b3a4 100644
--- a/src/com/android/contacts/group/GroupMembersFragment.java
+++ b/src/com/android/contacts/group/GroupMembersFragment.java
@@ -398,7 +398,7 @@
     }
 
     private void sendToGroup(long[] ids, String sendScheme, String title) {
-        if(ids == null || ids.length == 0) return;
+        if (ids == null || ids.length == 0) return;
 
         // Get emails or phone numbers
         // contactMap <contact_id, contact_data>
diff --git a/src/com/android/contacts/util/SharedPreferenceUtil.java b/src/com/android/contacts/util/SharedPreferenceUtil.java
index cee54b9..a1767fa 100644
--- a/src/com/android/contacts/util/SharedPreferenceUtil.java
+++ b/src/com/android/contacts/util/SharedPreferenceUtil.java
@@ -157,15 +157,19 @@
         final Set<String> imported = new HashSet<>(getImportedSims(context));
         final Set<String> dismissed = new HashSet<>(getDismissedSims(context));
         for (SimCard sim : sims) {
+            final String id = sim.getSimId();
+            if (id == null) {
+                continue;
+            }
             if (sim.isImported()) {
-                imported.add(sim.getSimId());
+                imported.add(id);
             } else {
-                imported.remove(sim.getSimId());
+                imported.remove(id);
             }
             if (sim.isDismissed()) {
-                dismissed.add(sim.getSimId());
+                dismissed.add(id);
             } else {
-                dismissed.remove(sim.getSimId());
+                dismissed.remove(id);
             }
         }
         getSharedPreferences(context).edit()
diff --git a/tests/src/com/android/contacts/common/model/SimContactTests.java b/tests/src/com/android/contacts/common/model/SimContactTests.java
index de9ab5a..c37c270 100644
--- a/tests/src/com/android/contacts/common/model/SimContactTests.java
+++ b/tests/src/com/android/contacts/common/model/SimContactTests.java
@@ -21,7 +21,7 @@
     public void parcelRoundtrip() {
         assertParcelsCorrectly(new SimContact(1, "name1", "phone1",
                 new String[] { "email1a", "email1b" }));
-        assertParcelsCorrectly(new SimContact(2, "name2", "phone2", null));
+        assertParcelsCorrectly(new SimContact(2, "name2", "phone2", null, 2));
         assertParcelsCorrectly(new SimContact(3, "name3", null,
                 new String[] { "email3" }));
         assertParcelsCorrectly(new SimContact(4, null, "phone4",
diff --git a/tests/src/com/android/contacts/tests/AdbHelpers.java b/tests/src/com/android/contacts/tests/AdbHelpers.java
index 0024dd1..163d7cc 100644
--- a/tests/src/com/android/contacts/tests/AdbHelpers.java
+++ b/tests/src/com/android/contacts/tests/AdbHelpers.java
@@ -16,10 +16,8 @@
 package com.android.contacts.tests;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.os.Build;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.support.annotation.RequiresApi;
 import android.support.test.InstrumentationRegistry;
 import android.util.Log;