Merge changes from topics 'sim-tests', 'import-notification' into ub-contactsdialer-i-dev

* changes:
  Add additional tests of SIM contact importing
  DO NOT MERGE Improve testability of SIM import code.
  DO NOT MERGE Add notification for SIM import
diff --git a/AndroidManifest_common.xml b/AndroidManifest_common.xml
index b2a3c88..2a0e3a0 100644
--- a/AndroidManifest_common.xml
+++ b/AndroidManifest_common.xml
@@ -394,6 +394,11 @@
             android:name=".ContactSaveService"
             android:exported="false" />
 
+         <!-- Service to import contacts from the SIM card -->
+         <service
+             android:name=".SimImportService"
+             android:exported="false" />
+
         <!-- Attaches a photo to a contact. Started from external applications -->
         <activity android:name=".activities.AttachPhotoActivity"
             android:label="@string/attach_photo_dialog_title"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f88be29..5a0610b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1904,4 +1904,13 @@
          [CHAR LIMIT=NONE] -->
     <string name="show_more_content_description">Show more</string>
 
+    <!-- The notification title shown when importing a SIM card finishes [CHAR LIMIT=40] -->
+    <string name="importing_sim_finished_title">Finished importing SIM card</string>
+
+    <!-- Notification title shown when importing SIM contacts failed [CHAR LIMIT=40] -->
+    <string name="importing_sim_failed_title">Couldn\'t import SIM card</string>
+
+    <!-- The notification title shown while SIM contacts are being imported [CHAR LIMIT=40] -->
+    <string name="importing_sim_in_progress_title">Importing SIM</string>
+
 </resources>
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 6c44a42..76a77c0 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -16,8 +16,6 @@
 
 package com.android.contacts;
 
-import static android.Manifest.permission.WRITE_CONTACTS;
-
 import android.app.Activity;
 import android.app.IntentService;
 import android.content.ContentProviderOperation;
@@ -49,7 +47,6 @@
 import android.provider.ContactsContract.RawContactsEntity;
 import android.support.v4.content.LocalBroadcastManager;
 import android.support.v4.os.ResultReceiver;
-import android.telephony.SubscriptionInfo;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
@@ -63,8 +60,6 @@
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.RawContactModifier;
-import com.android.contacts.common.model.SimCard;
-import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.ContactDisplayUtils;
@@ -72,7 +67,6 @@
 import com.android.contacts.compat.PinnedPositionsCompat;
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contactsbind.FeedbackHelper;
-
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
@@ -82,6 +76,8 @@
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import static android.Manifest.permission.WRITE_CONTACTS;
+
 /**
  * A service responsible for saving changes to the content provider.
  */
@@ -149,16 +145,11 @@
     public static final String EXTRA_UNDO_ACTION = "undoAction";
     public static final String EXTRA_UNDO_DATA = "undoData";
 
-    public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
-    public static final String EXTRA_SIM_CONTACTS = "simContacts";
-    public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
-
     // For debugging and testing what happens when requests are queued up.
     public static final String ACTION_SLEEP = "sleep";
     public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
 
     public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
-    public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
     public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
     public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
 
@@ -166,7 +157,6 @@
 
     public static final String EXTRA_RESULT_CODE = "resultCode";
     public static final String EXTRA_RESULT_COUNT = "count";
-    public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
 
     public static final int CP2_ERROR = 0;
     public static final int CONTACTS_LINKED = 1;
@@ -361,8 +351,6 @@
             setRingtone(intent);
         } else if (ACTION_UNDO.equals(action)) {
             undo(intent);
-        } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
-            importFromSim(intent);
         } else if (ACTION_SLEEP.equals(action)) {
             sleepForDebugging(intent);
         }
@@ -1752,58 +1740,6 @@
     }
 
     /**
-     * Returns an intent that can be used to import the contacts into targetAccount.
-     *
-     * @param context context to use for creating the intent
-     * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
-     *                       {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
-     *                       SIM for that subscription ID will be marked as imported
-     * @param contacts the contacts to import
-     * @param targetAccount the account import the contacts into
-     */
-    public static Intent createImportFromSimIntent(Context context, int subscriptionId,
-            ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
-        return new Intent(context, ContactSaveService.class)
-                .setAction(ACTION_IMPORT_FROM_SIM)
-                .putExtra(EXTRA_SIM_CONTACTS, contacts)
-                .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
-                .putExtra(EXTRA_ACCOUNT, targetAccount);
-    }
-
-    private void importFromSim(Intent intent) {
-        final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
-                .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
-        final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
-                SimCard.NO_SUBSCRIPTION_ID);
-        try {
-            final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
-            final ArrayList<SimContact> contacts =
-                    intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
-            mSimContactDao.importContacts(contacts, targetAccount);
-
-            // Update the imported state of the SIM card that was imported
-            final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
-            if (sim != null) {
-                mSimContactDao.persistSimState(sim.withImportedState(true));
-            }
-
-            // notify success
-            LocalBroadcastManager.getInstance(this).sendBroadcast(result
-                    .putExtra(EXTRA_RESULT_COUNT, contacts.size())
-                    .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
-                    .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "importFromSim completed successfully");
-            }
-        } catch (RemoteException|OperationApplicationException e) {
-            FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
-            LocalBroadcastManager.getInstance(this).sendBroadcast(result
-                    .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
-                    .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
-        }
-    }
-
-    /**
      * Returns an intent that can start this service and cause it to sleep for the specified time.
      *
      * This exists purely for debugging and manual testing. Since this service uses a single thread
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index b512599..dc4cabf 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -281,10 +281,8 @@
                 importableContacts.add(mAdapter.getItem(checked.keyAt(i)));
             }
         }
-        ContactSaveService.startService(getContext(), ContactSaveService
-                .createImportFromSimIntent(getContext(), mSubscriptionId,
-                        importableContacts,
-                        mAccountHeaderPresenter.getCurrentAccount()));
+        SimImportService.startImport(getContext(), mSubscriptionId, importableContacts,
+                mAccountHeaderPresenter.getCurrentAccount());
     }
 
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
diff --git a/src/com/android/contacts/SimImportService.java b/src/com/android/contacts/SimImportService.java
new file mode 100644
index 0000000..4fa3695
--- /dev/null
+++ b/src/com/android/contacts/SimImportService.java
@@ -0,0 +1,341 @@
+/*
+ * 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.
+ */
+package com.android.contacts;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.TimingLogger;
+
+import com.android.contacts.activities.PeopleActivity;
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.database.SimContactDaoImpl;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contactsbind.FeedbackHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Imports {@link SimContact}s from a background thread
+ */
+public class SimImportService extends Service {
+
+    private static final String TAG = "SimImportService";
+
+    /**
+     * Wrapper around the service state for testability
+     */
+    public interface StatusProvider {
+
+        /**
+         * Returns whether there is any imports still pending
+         *
+         * <p>This should be called from the UI thread</p>
+         */
+        boolean isRunning();
+
+        /**
+         * Returns whether an import for sim has been requested
+         *
+         * <p>This should be called from the UI thread</p>
+         */
+        boolean isImporting(SimCard sim);
+    }
+
+    public static final String EXTRA_ACCOUNT = "account";
+    public static final String EXTRA_SIM_CONTACTS = "simContacts";
+    public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
+    public static final String EXTRA_RESULT_CODE = "resultCode";
+    public static final String EXTRA_RESULT_COUNT = "count";
+    public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
+
+    public static final String BROADCAST_SERVICE_STATE_CHANGED =
+            SimImportService.class.getName() + "#serviceStateChanged";
+    public static final String BROADCAST_SIM_IMPORT_COMPLETE =
+            SimImportService.class.getName() + "#simImportComplete";
+
+    public static final int RESULT_UNKNOWN = 0;
+    public static final int RESULT_SUCCESS = 1;
+    public static final int RESULT_FAILURE = 2;
+
+    // VCardService uses jobIds for it's notifications which count up from 0 so we just use a
+    // bigger number to prevent overlap.
+    private static final int NOTIFICATION_ID = 100;
+
+    private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+    // Keeps track of current tasks. This is only modified from the UI thread.
+    private static List<ImportTask> sPending = new ArrayList<>();
+
+    private static StatusProvider sStatusProvider = new StatusProvider() {
+        @Override
+        public boolean isRunning() {
+            return !sPending.isEmpty();
+        }
+
+        @Override
+        public boolean isImporting(SimCard sim) {
+            return SimImportService.isImporting(sim);
+        }
+    };
+
+    /**
+     * Returns whether an import for sim has been requested
+     *
+     * <p>This should be called from the UI thread</p>
+     */
+    private static boolean isImporting(SimCard sim) {
+        for (ImportTask task : sPending) {
+            if (task.getSim().equals(sim)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static StatusProvider getStatusProvider() {
+        return sStatusProvider;
+    }
+
+    /**
+     * Starts an import of the contacts from the sim into the target account
+     *
+     * @param context context to use for starting the service
+     * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
+     *                       {@link android.telephony.SubscriptionInfo#getSubscriptionId()}.
+     *                       Upon completion the SIM for that subscription ID will be marked as
+     *                       imported
+     * @param contacts the contacts to import
+     * @param targetAccount the account import the contacts into
+     */
+    public static void startImport(Context context, int subscriptionId,
+            ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
+        context.startService(new Intent(context, SimImportService.class)
+                .putExtra(EXTRA_SIM_CONTACTS, contacts)
+                .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
+                .putExtra(EXTRA_ACCOUNT, targetAccount));
+    }
+
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, final int startId) {
+        final ImportTask task = createTaskForIntent(intent, startId);
+        if (task == null) {
+            new StopTask(this, startId).executeOnExecutor(mExecutor);
+            return START_NOT_STICKY;
+        }
+        sPending.add(task);
+        task.executeOnExecutor(mExecutor);
+        notifyStateChanged();
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mExecutor.shutdown();
+    }
+
+    private ImportTask createTaskForIntent(Intent intent, int startId) {
+        final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
+        final ArrayList<SimContact> contacts =
+                intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
+
+        final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
+                SimCard.NO_SUBSCRIPTION_ID);
+        final SimContactDao dao = SimContactDao.create(this);
+        final SimCard sim = dao.getSimBySubscriptionId(subscriptionId);
+        if (sim != null) {
+            return new ImportTask(sim, contacts, targetAccount, dao, startId);
+        } else {
+            return null;
+        }
+    }
+
+    private Notification getCompletedNotification() {
+        final Intent intent = new Intent(this, PeopleActivity.class);
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+        builder.setOngoing(false)
+                .setAutoCancel(true)
+                .setContentTitle(this.getString(R.string.importing_sim_finished_title))
+                .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(R.drawable.ic_check_mark)
+                .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
+        return builder.build();
+    }
+
+    private Notification getFailedNotification() {
+        final Intent intent = new Intent(this, PeopleActivity.class);
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+        builder.setOngoing(false)
+                .setAutoCancel(true)
+                .setContentTitle(this.getString(R.string.importing_sim_failed_title))
+                .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(R.drawable.ic_check_mark)
+                .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
+        return builder.build();
+    }
+
+    private Notification getImportingNotification() {
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+        final String description = getString(R.string.importing_sim_in_progress_title);
+        builder.setOngoing(true)
+                .setProgress(/* current */ 0, /* max */ 100, /* indeterminate */ true)
+                .setContentTitle(description)
+                .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(android.R.drawable.stat_sys_download);
+        return builder.build();
+    }
+
+    private void notifyStateChanged() {
+        LocalBroadcastManager.getInstance(this).sendBroadcast(
+                new Intent(BROADCAST_SERVICE_STATE_CHANGED));
+    }
+
+    // Schedule a task that calls stopSelf when it completes. This is used to ensure that the
+    // calls to stopSelf occur in the correct order (because this service uses a single thread
+    // executor this won't run until all work that was requested before it has finished)
+    private static class StopTask extends AsyncTask<Void, Void, Void> {
+        private Service mHost;
+        private final int mStartId;
+
+        private StopTask(Service host, int startId) {
+            mHost = host;
+            mStartId = startId;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            super.onPostExecute(aVoid);
+            mHost.stopSelf(mStartId);
+        }
+    }
+
+    private class ImportTask extends AsyncTask<Void, Void, Boolean> {
+        private final SimCard mSim;
+        private final List<SimContact> mContacts;
+        private final AccountWithDataSet mTargetAccount;
+        private final SimContactDao mDao;
+        private final NotificationManager mNotificationManager;
+        private final int mStartId;
+        private final long mStartTime;
+
+        public ImportTask(SimCard sim, List<SimContact> contacts, AccountWithDataSet targetAccount,
+                SimContactDao dao, int startId) {
+            mSim = sim;
+            mContacts = contacts;
+            mTargetAccount = targetAccount;
+            mDao = dao;
+            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+            mStartId = startId;
+            mStartTime = System.currentTimeMillis();
+        }
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+            startForeground(NOTIFICATION_ID, getImportingNotification());
+        }
+
+        @Override
+        protected Boolean doInBackground(Void... params) {
+            final TimingLogger timer = new TimingLogger(TAG, "import");
+            try {
+                // Just import them all at once.
+                // Experimented with using smaller batches (e.g. 25 and 50) so that percentage
+                // progress could be displayed however this slowed down the import by over a factor
+                // of 2. If the batch size is over a 100 then most cases will only require a single
+                // batch so we don't even worry about displaying accurate progress
+                mDao.importContacts(mContacts, mTargetAccount);
+                mDao.persistSimState(mSim.withImportedState(true));
+                timer.addSplit("done");
+                timer.dumpToLog();
+            } catch (RemoteException|OperationApplicationException e) {
+                FeedbackHelper.sendFeedback(SimImportService.this, TAG,
+                        "Failed to import contacts from SIM card", e);
+                return false;
+            }
+            return true;
+        }
+
+        public SimCard getSim() {
+            return mSim;
+        }
+
+        @Override
+        protected void onPostExecute(Boolean success) {
+            super.onPostExecute(success);
+            stopSelf(mStartId);
+
+            Intent result;
+            final Notification notification;
+            if (success) {
+                result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
+                        .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
+                        .putExtra(EXTRA_RESULT_COUNT, mContacts.size())
+                        .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
+                        .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
+
+                notification = getCompletedNotification();
+            } else {
+                result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
+                        .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
+                        .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
+                        .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
+
+                notification = getFailedNotification();
+            }
+            LocalBroadcastManager.getInstance(SimImportService.this).sendBroadcast(result);
+
+            sPending.remove(this);
+
+            // Only notify of completion if all the import requests have finished. We're using
+            // the same notification for imports so in the rare case that a user has started
+            // multiple imports the notification won't go away until all of them complete.
+            if (sPending.isEmpty()) {
+                stopForeground(false);
+                mNotificationManager.notify(NOTIFICATION_ID, notification);
+            }
+            notifyStateChanged();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index b5b6626..b7b3ae4 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -15,46 +15,19 @@
  */
 package com.android.contacts.common.database;
 
-import android.annotation.TargetApi;
-import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.OperationApplicationException;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Build;
 import android.os.RemoteException;
-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;
-import android.support.v4.util.ArraySet;
-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;
-import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.model.SimCard;
 import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.util.SharedPreferenceUtil;
-import com.google.common.base.Joiner;
 
 import java.util.ArrayList;
-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;
@@ -63,349 +36,15 @@
  * Provides data access methods for loading contacts from a SIM card and and migrating these
  * SIM contacts to a CP2 account.
  */
-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;
-
-    // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
-    // query parameter limit.
-    static final int QUERY_MAX_BATCH_SIZE = 100;
+public abstract class SimContactDao {
 
     // 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;
 
-    @VisibleForTesting
-    public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
-
-    public static String _ID = BaseColumns._ID;
-    public static String NAME = "name";
-    public static String NUMBER = "number";
-    public static String EMAILS = "emails";
-
-    private final Context mContext;
-    private final ContentResolver mResolver;
-    private final TelephonyManager mTelephonyManager;
-
-    private SimContactDao(Context context) {
-        mContext = context;
-        mResolver = context.getContentResolver();
-        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-    }
-
-
-    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_READY;
-    }
-
-    public List<SimCard> getSimCards() {
-        if (!canReadSimContacts()) {
-            return Collections.emptyList();
-        }
-        final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
-                getSimCardsFromSubscriptions() :
-                Collections.singletonList(SimCard.create(mTelephonyManager,
-                        mContext.getString(R.string.single_sim_display_label)));
-        return SharedPreferenceUtil.restoreSimStates(mContext, sims);
-    }
-
-    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")
-                .appendPath(String.valueOf(subscriptionId))
-                .build());
-    }
-
-    public ArrayList<SimContact> loadSimContacts() {
-        return loadFrom(ICC_CONTENT_URI);
-    }
-
-    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 = SharedPreferenceUtil.restoreSimStates(mContext, 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;
-    }
-
-    /**
-     * 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<>();
-        for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
-            findAccountsOfExistingSimContacts(
-                    contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
-                    result);
-        }
-        return result;
-    }
-
-    private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
-            Map<AccountWithDataSet, Set<SimContact>> result) {
-        final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
-        Collections.sort(contacts, SimContact.compareByPhoneThenName());
-
-        final Cursor dataCursor = queryRawContactsForSimContacts(contacts);
-
-        try {
-            while (dataCursor.moveToNext()) {
-                final String number = DataQuery.getPhoneNumber(dataCursor);
-                final String name = DataQuery.getDisplayName(dataCursor);
-
-                final int index = SimContact.findByPhoneAndName(contacts, number, name);
-                if (index < 0) {
-                    continue;
-                }
-                final SimContact contact = contacts.get(index);
-                final long id = DataQuery.getRawContactId(dataCursor);
-                if (!rawContactToSimContact.containsKey(id)) {
-                    rawContactToSimContact.put(id, new ArrayList<SimContact>());
-                }
-                rawContactToSimContact.get(id).add(contact);
-            }
-        } finally {
-            dataCursor.close();
-        }
-
-        final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
-        try {
-            while (accountsCursor.moveToNext()) {
-                final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
-                final long id = AccountQuery.getId(accountsCursor);
-                if (!result.containsKey(account)) {
-                    result.put(account, new ArraySet<SimContact>());
-                }
-                for (SimContact contact : rawContactToSimContact.get(id)) {
-                    result.get(account).add(contact);
-                }
-            }
-        } finally {
-            accountsCursor.close();
-        }
-    }
-
-    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();
-        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);
-    }
-
-    // See b/32831092
-    // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
-    // concurrently. So we just have a global lock around it to prevent potential issues.
-    private static final Object SIM_READ_LOCK = new Object();
-    private ArrayList<SimContact> loadFrom(Uri uri) {
-        synchronized (SIM_READ_LOCK) {
-            final Cursor cursor = mResolver.query(uri, null, null, null, null);
-
-            try {
-                return loadFromCursor(cursor);
-            } finally {
-                cursor.close();
-            }
-        }
-    }
-
-    private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
-        final int colId = cursor.getColumnIndex(_ID);
-        final int colName = cursor.getColumnIndex(NAME);
-        final int colNumber = cursor.getColumnIndex(NUMBER);
-        final int colEmails = cursor.getColumnIndex(EMAILS);
-
-        final ArrayList<SimContact> result = new ArrayList<>();
-
-        while (cursor.moveToNext()) {
-            final long id = cursor.getLong(colId);
-            final String name = cursor.getString(colName);
-            final String number = cursor.getString(colNumber);
-            final String emails = cursor.getString(colEmails);
-
-            final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
-            result.add(contact);
-        }
-        return result;
-    }
-
-    private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
-        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('(');
-        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(')');
-        for (SimContact contact : contacts) {
-            if (contact.hasPhone()) {
-                selectionArgs.add(contact.getPhone());
-            }
-        }
-        selectionBuilder.append(')');
-
-        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(),
-                selectionArgs.toArray(new String[selectionArgs.size()]),
-                null);
-    }
-
-    private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
-        final StringBuilder selectionBuilder = new StringBuilder();
-
-        final String[] args = new String[ids.size()];
-
-        selectionBuilder.append(RawContacts._ID).append(" IN (")
-                .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
-                .append(")");
-        int i = 0;
-        for (long id : ids) {
-            args[i++] = String.valueOf(id);
-        }
-        return mResolver.query(RawContacts.CONTENT_URI,
-                AccountQuery.PROJECTION,
-                selectionBuilder.toString(),
-                args,
-                null);
-    }
-
-    private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
-            AccountWithDataSet targetAccount) {
-        final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
-        for (SimContact contact : contacts) {
-            contact.appendCreateContactOperations(ops, targetAccount);
-        }
-        return ops;
-    }
-
-    private String[] parseEmails(String emails) {
-        return emails != null ? emails.split(",") : null;
-    }
-
-    private boolean hasTelephony() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
-    }
-
-    private boolean hasPermissions() {
-        return PermissionsUtil.hasContactsPermissions(mContext) &&
-                PermissionsUtil.hasPhonePermissions(mContext);
-    }
-
     public static SimContactDao create(Context context) {
         if (USE_FAKE_INSTANCE) {
-            return new DebugImpl(context)
+            return new SimContactDaoImpl.DebugImpl(context)
                     .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier",
                             "Card 1", "15095550101", "us").withContacts(
                             new SimContact(1, "Sim One", "15095550111", null),
@@ -423,89 +62,27 @@
                             new SimContact(5, "Sim Duplicate", "15095550121", null)
                     ));
         }
-        return new SimContactDao(context);
+        return new SimContactDaoImpl(context);
     }
 
-    // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
-    // active development or anytime after 3/1/2017
-    public static class DebugImpl extends SimContactDao {
+    public abstract boolean canReadSimContacts();
 
-        private List<SimCard> mSimCards = new ArrayList<>();
-        private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
+    public abstract List<SimCard> getSimCards();
 
-        public DebugImpl(Context context) {
-            super(context);
-        }
+    public abstract ArrayList<SimContact> loadContactsForSim(SimCard sim);
 
-        public DebugImpl addSimCard(SimCard sim) {
-            mSimCards.add(sim);
-            mCardsBySubscription.put(sim.getSubscriptionId(), sim);
-            return this;
-        }
+    public abstract ContentProviderResult[] importContacts(List<SimContact> contacts,
+            AccountWithDataSet targetAccount)
+            throws RemoteException, OperationApplicationException;
 
-        @Override
-        public List<SimCard> getSimCards() {
-            return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
-        }
+    public abstract void persistSimStates(List<SimCard> simCards);
 
-        @Override
-        public ArrayList<SimContact> loadSimContacts() {
-            return new ArrayList<>(mSimCards.get(0).getContacts());
-        }
+    public abstract SimCard getSimBySubscriptionId(int subscriptionId);
 
-        @Override
-        public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
-            return new ArrayList<>(mCardsBySubscription.get(subscriptionId).getContacts());
-        }
+    public abstract Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
+            List<SimContact> contacts);
 
-        @Override
-        public boolean canReadSimContacts() {
-            return true;
-        }
-    }
-
-    // Query used for detecting existing contacts that may match a SimContact.
-    private static final class DataQuery {
-
-        public static final String[] PROJECTION = new String[] {
-                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 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 {
-        public static final String[] PROJECTION = new String[] {
-                RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
-                RawContacts.DATA_SET
-        };
-
-        public static long getId(Cursor cursor) {
-            return cursor.getLong(0);
-        }
-
-        public static AccountWithDataSet getAccount(Cursor cursor) {
-            return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
-                    cursor.getString(3));
-        }
+    public void persistSimState(SimCard sim) {
+        persistSimStates(Collections.singletonList(sim));
     }
 }
diff --git a/src/com/android/contacts/common/database/SimContactDaoImpl.java b/src/com/android/contacts/common/database/SimContactDaoImpl.java
new file mode 100644
index 0000000..6ddf663
--- /dev/null
+++ b/src/com/android/contacts/common/database/SimContactDaoImpl.java
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+package com.android.contacts.common.database;
+
+import android.annotation.TargetApi;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.RemoteException;
+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;
+import android.support.v4.util.ArraySet;
+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;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.util.SharedPreferenceUtil;
+import com.google.common.base.Joiner;
+
+import java.util.ArrayList;
+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;
+
+/**
+ * Provides data access methods for loading contacts from a SIM card and and migrating these
+ * SIM contacts to a CP2 account.
+ */
+public class SimContactDaoImpl extends 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;
+
+    // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+    // query parameter limit.
+    static final int QUERY_MAX_BATCH_SIZE = 100;
+
+    @VisibleForTesting
+    public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
+
+    public static String _ID = BaseColumns._ID;
+    public static String NAME = "name";
+    public static String NUMBER = "number";
+    public static String EMAILS = "emails";
+
+    private final Context mContext;
+    private final ContentResolver mResolver;
+    private final TelephonyManager mTelephonyManager;
+
+    public SimContactDaoImpl(Context context) {
+        mContext = context;
+        mResolver = context.getContentResolver();
+        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    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_READY;
+    }
+
+    @Override
+    public List<SimCard> getSimCards() {
+        if (!canReadSimContacts()) {
+            return Collections.emptyList();
+        }
+        final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
+                getSimCardsFromSubscriptions() :
+                Collections.singletonList(SimCard.create(mTelephonyManager,
+                        mContext.getString(R.string.single_sim_display_label)));
+        return SharedPreferenceUtil.restoreSimStates(mContext, sims);
+    }
+
+    @Override
+    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")
+                .appendPath(String.valueOf(subscriptionId))
+                .build());
+    }
+
+    public ArrayList<SimContact> loadSimContacts() {
+        return loadFrom(ICC_CONTENT_URI);
+    }
+
+    @Override
+    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));
+    }
+
+    @Override
+    public void persistSimStates(List<SimCard> simCards) {
+        SharedPreferenceUtil.persistSimStates(mContext, simCards);
+    }
+
+    @Override
+    public SimCard getSimBySubscriptionId(int subscriptionId) {
+        final List<SimCard> sims = SharedPreferenceUtil.restoreSimStates(mContext, 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;
+    }
+
+    /**
+     * 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<>();
+        for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
+            findAccountsOfExistingSimContacts(
+                    contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
+                    result);
+        }
+        return result;
+    }
+
+    private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
+            Map<AccountWithDataSet, Set<SimContact>> result) {
+        final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
+        Collections.sort(contacts, SimContact.compareByPhoneThenName());
+
+        final Cursor dataCursor = queryRawContactsForSimContacts(contacts);
+
+        try {
+            while (dataCursor.moveToNext()) {
+                final String number = DataQuery.getPhoneNumber(dataCursor);
+                final String name = DataQuery.getDisplayName(dataCursor);
+
+                final int index = SimContact.findByPhoneAndName(contacts, number, name);
+                if (index < 0) {
+                    continue;
+                }
+                final SimContact contact = contacts.get(index);
+                final long id = DataQuery.getRawContactId(dataCursor);
+                if (!rawContactToSimContact.containsKey(id)) {
+                    rawContactToSimContact.put(id, new ArrayList<SimContact>());
+                }
+                rawContactToSimContact.get(id).add(contact);
+            }
+        } finally {
+            dataCursor.close();
+        }
+
+        final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
+        try {
+            while (accountsCursor.moveToNext()) {
+                final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
+                final long id = AccountQuery.getId(accountsCursor);
+                if (!result.containsKey(account)) {
+                    result.put(account, new ArraySet<SimContact>());
+                }
+                for (SimContact contact : rawContactToSimContact.get(id)) {
+                    result.get(account).add(contact);
+                }
+            }
+        } finally {
+            accountsCursor.close();
+        }
+    }
+
+
+    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();
+        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);
+    }
+
+    // See b/32831092
+    // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
+    // concurrently. So we just have a global lock around it to prevent potential issues.
+    private static final Object SIM_READ_LOCK = new Object();
+    private ArrayList<SimContact> loadFrom(Uri uri) {
+        synchronized (SIM_READ_LOCK) {
+            final Cursor cursor = mResolver.query(uri, null, null, null, null);
+
+            try {
+                return loadFromCursor(cursor);
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
+        final int colId = cursor.getColumnIndex(_ID);
+        final int colName = cursor.getColumnIndex(NAME);
+        final int colNumber = cursor.getColumnIndex(NUMBER);
+        final int colEmails = cursor.getColumnIndex(EMAILS);
+
+        final ArrayList<SimContact> result = new ArrayList<>();
+
+        while (cursor.moveToNext()) {
+            final long id = cursor.getLong(colId);
+            final String name = cursor.getString(colName);
+            final String number = cursor.getString(colNumber);
+            final String emails = cursor.getString(colEmails);
+
+            final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
+            // Only include contact if it has some useful data
+            if (contact.hasName() || contact.hasPhone() || contact.hasEmails()) {
+                result.add(contact);
+            }
+        }
+        return result;
+    }
+
+    private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
+        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('(');
+        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(')');
+        for (SimContact contact : contacts) {
+            if (contact.hasPhone()) {
+                selectionArgs.add(contact.getPhone());
+            }
+        }
+        selectionBuilder.append(')');
+
+        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(),
+                selectionArgs.toArray(new String[selectionArgs.size()]),
+                null);
+    }
+
+    private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
+        final StringBuilder selectionBuilder = new StringBuilder();
+
+        final String[] args = new String[ids.size()];
+
+        selectionBuilder.append(RawContacts._ID).append(" IN (")
+                .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
+                .append(")");
+        int i = 0;
+        for (long id : ids) {
+            args[i++] = String.valueOf(id);
+        }
+        return mResolver.query(RawContacts.CONTENT_URI,
+                AccountQuery.PROJECTION,
+                selectionBuilder.toString(),
+                args,
+                null);
+    }
+
+    private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
+            AccountWithDataSet targetAccount) {
+        final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+        for (SimContact contact : contacts) {
+            contact.appendCreateContactOperations(ops, targetAccount);
+        }
+        return ops;
+    }
+
+    private String[] parseEmails(String emails) {
+        return !TextUtils.isEmpty(emails) ? emails.split(",") : null;
+    }
+
+    private boolean hasTelephony() {
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+    }
+
+    private boolean hasPermissions() {
+        return PermissionsUtil.hasContactsPermissions(mContext) &&
+                PermissionsUtil.hasPhonePermissions(mContext);
+    }
+
+    // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
+    // active development or anytime after 3/1/2017
+    public static class DebugImpl extends SimContactDaoImpl {
+
+        private List<SimCard> mSimCards = new ArrayList<>();
+        private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
+
+        public DebugImpl(Context context) {
+            super(context);
+        }
+
+        public DebugImpl addSimCard(SimCard sim) {
+            mSimCards.add(sim);
+            mCardsBySubscription.put(sim.getSubscriptionId(), sim);
+            return this;
+        }
+
+        @Override
+        public List<SimCard> getSimCards() {
+            return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
+        }
+
+        @Override
+        public ArrayList<SimContact> loadContactsForSim(SimCard card) {
+            return new ArrayList<>(card.getContacts());
+        }
+
+        @Override
+        public boolean canReadSimContacts() {
+            return true;
+        }
+    }
+
+    // Query used for detecting existing contacts that may match a SimContact.
+    private static final class DataQuery {
+
+        public static final String[] PROJECTION = new String[] {
+                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 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 {
+        public static final String[] PROJECTION = new String[] {
+                RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+                RawContacts.DATA_SET
+        };
+
+        public static long getId(Cursor cursor) {
+            return cursor.getLong(0);
+        }
+
+        public static AccountWithDataSet getAccount(Cursor cursor) {
+            return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
+                    cursor.getString(3));
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/SimCard.java b/src/com/android/contacts/common/model/SimCard.java
index 7b13096..3e826dd 100644
--- a/src/com/android/contacts/common/model/SimCard.java
+++ b/src/com/android/contacts/common/model/SimCard.java
@@ -221,6 +221,20 @@
         return result;
     }
 
+    @Override
+    public String toString() {
+        return "SimCard{" +
+                "mSimId='" + mSimId + '\'' +
+                ", mSubscriptionId=" + mSubscriptionId +
+                ", mCarrierName=" + mCarrierName +
+                ", mDisplayName=" + mDisplayName +
+                ", mPhoneNumber='" + mPhoneNumber + '\'' +
+                ", mCountryCode='" + mCountryCode + '\'' +
+                ", mDismissed=" + mDismissed +
+                ", mImported=" + mImported +
+                ", mContacts=" + mContacts +
+                '}';
+    }
 
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
     public static SimCard create(SubscriptionInfo info) {
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
index 2d26029..7442805 100644
--- a/src/com/android/contacts/common/model/SimContact.java
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -24,6 +24,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
 
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.google.common.collect.ComparisonChain;
@@ -45,6 +46,10 @@
     private final String mPhone;
     private final String[] mEmails;
 
+    public SimContact(long id, String name, String phone) {
+        this(id, name, phone, null);
+    }
+
     public SimContact(long id, String name, String phone, String[] emails) {
         mId = id;
         mName = name;
@@ -52,6 +57,10 @@
         mEmails = emails;
     }
 
+    public SimContact(SimContact other) {
+        this(other.mId, other.mName, other.mPhone, other.mEmails);
+    }
+
     public long getId() {
         return mId;
     }
@@ -112,7 +121,7 @@
     }
 
     public boolean hasName() {
-        return mName != null;
+        return !TextUtils.isEmpty(mName);
     }
 
     public boolean hasPhone() {
diff --git a/src/com/android/contacts/common/model/account/AccountWithDataSet.java b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
index 3ee0aab..304dca7 100644
--- a/src/com/android/contacts/common/model/account/AccountWithDataSet.java
+++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -17,6 +17,7 @@
 package com.android.contacts.common.model.account;
 
 import android.accounts.Account;
+import android.content.ContentProviderOperation;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
@@ -191,6 +192,20 @@
     }
 
     /**
+     * Returns a {@link ContentProviderOperation} that will create a RawContact in this account
+     */
+    public ContentProviderOperation newRawContactOperation() {
+        final ContentProviderOperation.Builder builder =
+                ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                        .withValue(RawContacts.ACCOUNT_NAME, name)
+                        .withValue(RawContacts.ACCOUNT_TYPE, type);
+        if (dataSet != null) {
+            builder.withValue(RawContacts.DATA_SET, dataSet);
+        }
+        return builder.build();
+    }
+
+    /**
      * Unpack a string created by {@link #stringify}.
      *
      * @throws IllegalArgumentException if it's an invalid string.
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index e2782f8..08846a8 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -43,8 +43,8 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
-import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
+import com.android.contacts.SimImportService;
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.compat.TelecomManagerUtil;
 import com.android.contacts.common.compat.TelephonyManagerCompat;
@@ -192,7 +192,7 @@
         mSaveServiceListener = new SaveServiceResultListener();
         LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
                 mSaveServiceListener,
-                new IntentFilter(ContactSaveService.BROADCAST_SIM_IMPORT_COMPLETE));
+                new IntentFilter(SimImportService.BROADCAST_SIM_IMPORT_COMPLETE));
     }
 
     @Override
@@ -401,20 +401,20 @@
         public void onReceive(Context context, Intent intent) {
             final long now = System.currentTimeMillis();
             final long opStart = intent.getLongExtra(
-                    ContactSaveService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
+                    SimImportService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
 
             // If it's been over 30 seconds the user is likely in a different context so suppress
             // the toast message.
             if (now - opStart > 30*1000) return;
 
-            final int code = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_CODE,
-                    ContactSaveService.RESULT_UNKNOWN);
-            final int count = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_COUNT, -1);
-            if (code == ContactSaveService.RESULT_SUCCESS && count > 0) {
+            final int code = intent.getIntExtra(SimImportService.EXTRA_RESULT_CODE,
+                    SimImportService.RESULT_UNKNOWN);
+            final int count = intent.getIntExtra(SimImportService.EXTRA_RESULT_COUNT, -1);
+            if (code == SimImportService.RESULT_SUCCESS && count > 0) {
                 Snackbar.make(mRootView, getResources().getQuantityString(
                         R.plurals.sim_import_success_toast_fmt, count, count),
                         Snackbar.LENGTH_LONG).show();
-            } else if (code == ContactSaveService.RESULT_FAILURE) {
+            } else if (code == SimImportService.RESULT_FAILURE) {
                 Snackbar.make(mRootView, R.string.sim_import_failed_toast,
                         Snackbar.LENGTH_LONG).show();
             }
diff --git a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
index ec03f0f..4702442 100644
--- a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -18,28 +18,44 @@
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.database.CursorWrapper;
 import android.database.DatabaseUtils;
+import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
 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.SmallTest;
 import android.support.test.filters.Suppress;
 import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.view.Menu;
 
+import com.android.contacts.common.model.SimCard;
 import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.test.mocks.MockContentProvider;
 import com.android.contacts.tests.AccountsTestHelper;
 import com.android.contacts.tests.SimContactsTestHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.experimental.runners.Enclosed;
 import org.junit.runner.RunWith;
@@ -47,17 +63,32 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Random;
 import java.util.Set;
 
 import static android.os.Build.VERSION_CODES;
 import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 @RunWith(Enclosed.class)
 public class SimContactDaoTests {
 
+    // Some random area codes for generating realistic US phones when
+    // generating fake data for the SIM contacts or CP2
+    private static final String[] AREA_CODES = 
+            {"360", "509", "416", "831", "212", "208"};
+    private static final Random sRandom = new Random();
+
+    // Approximate maximum number of contacts that can be stored on a SIM card for testing
+    // boundary cases
+    public static final int MAX_SIM_CONTACTS = 600;
+
     // On pre-M addAccountExplicitly (which we call via AccountsTestHelper) causes a
     // SecurityException to be thrown unless we add AUTHENTICATE_ACCOUNTS permission to the app
     // manifest. Instead of adding the extra permission just for tests we'll just only run them
@@ -89,8 +120,8 @@
             final SimContactDao sut = SimContactDao.create(getContext());
 
             sut.importContacts(Arrays.asList(
-                    new SimContact(1, "Test One", "15095550101", null),
-                    new SimContact(2, "Test Two", "15095550102", null),
+                    new SimContact(1, "Test One", "15095550101"),
+                    new SimContact(2, "Test Two", "15095550102"),
                     new SimContact(3, "Test Three", "15095550103", new String[] {
                             "user@example.com", "user2@example.com"
                     })
@@ -169,6 +200,35 @@
             dataCursor.close();
         }
 
+        /**
+         * Tests importing a large number of contacts
+         *
+         * Make sure that {@link android.os.TransactionTooLargeException} is not thrown
+         */
+        @Test
+        public void largeImport() throws Exception {
+            final SimContactDao sut = SimContactDao.create(getContext());
+
+            final List<SimContact> contacts = new ArrayList<>();
+
+            for (int i = 0; i < MAX_SIM_CONTACTS; i++) {
+                contacts.add(new SimContact(i + 1, "Contact " + (i + 1), randomPhone(),
+                        new String[] { randomEmail("contact" + (i + 1) + "_")}));
+            }
+
+            sut.importContacts(contacts, mAccount);
+
+            final Cursor contactsCursor = queryAllRawContactsInAccount();
+            assertThat(contactsCursor, hasCount(MAX_SIM_CONTACTS));
+            contactsCursor.close();
+
+            final Cursor dataCursor = queryAllDataInAccount();
+            // Each contact has one data row for each of name, phone and email
+            assertThat(dataCursor, hasCount(MAX_SIM_CONTACTS * 3));
+
+            dataCursor.close();
+        }
+
         private Cursor queryAllRawContactsInAccount() {
             return new StringableCursor(mResolver.query(ContactsContract.RawContacts.CONTENT_URI,
                     null, ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
@@ -180,7 +240,7 @@
         }
 
         private Cursor queryAllDataInAccount() {
-            return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+            return new StringableCursor(mResolver.query(Data.CONTENT_URI, null,
                     ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
                             ContactsContract.RawContacts.ACCOUNT_TYPE+ "=?",
                     new String[] {
@@ -190,10 +250,10 @@
         }
 
         private Cursor queryContactWithName(String name) {
-            return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+            return new StringableCursor(mResolver.query(Data.CONTENT_URI, null,
                     ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
                             ContactsContract.RawContacts.ACCOUNT_TYPE+ "=? AND " +
-                            ContactsContract.Data.DISPLAY_NAME + "=?",
+                            Data.DISPLAY_NAME + "=?",
                     new String[] {
                             mAccount.name,
                             mAccount.type,
@@ -202,42 +262,73 @@
         }
     }
 
+    /**
+     * Tests for {@link SimContactDao#findAccountsOfExistingSimContacts(List)}
+     *
+     * These are integration tests that query CP2 so that the SQL will be validated in addition
+     * to the detection algorithm
+     */
     @SdkSuppress(minSdkVersion = VERSION_CODES.M)
     // Lollipop MR1 is required for removeAccountExplicitly
     @RequiresApi(api = VERSION_CODES.LOLLIPOP_MR1)
-    @MediumTest
+    @LargeTest
     @RunWith(AndroidJUnit4.class)
-    public static class ExistingContactsTest {
+    public static class FindAccountsIntegrationTests {
 
         private Context mContext;
         private AccountsTestHelper mAccountHelper;
-        private AccountWithDataSet mAccount;
+        private List<AccountWithDataSet> mAccounts;
         // We need to generate something distinct to prevent flakiness on devices that may not
         // start with an empty CP2 DB
-        private String mNameSuffix = "";
+        private String mNameSuffix;
+
+        private static AccountWithDataSet sSeedAccount;
+
+        @BeforeClass
+        public static void setUpClass() throws Exception {
+            final AccountsTestHelper helper = new AccountsTestHelper(
+                    InstrumentationRegistry.getContext());
+            sSeedAccount = helper.addTestAccount(helper.generateAccountName("seedAccount"));
+
+            seedCp2();
+        }
+
+        @AfterClass
+        public static void tearDownClass() {
+            final AccountsTestHelper helper = new AccountsTestHelper(
+                    InstrumentationRegistry.getContext());
+            helper.removeTestAccount(sSeedAccount);
+            sSeedAccount = null;
+        }
 
         @Before
-        public void setUp() {
+        public void setUp() throws Exception {
             mContext = InstrumentationRegistry.getTargetContext();
             mAccountHelper = new AccountsTestHelper(InstrumentationRegistry.getContext());
-            mAccount = mAccountHelper.addTestAccount();
-            mNameSuffix = "testAt" + System.nanoTime();
+            mAccounts = new ArrayList<>();
+            mNameSuffix = getClass().getSimpleName() + "At" + System.nanoTime();
+
+            seedCp2();
         }
 
         @After
         public void tearDown() {
-            mAccountHelper.cleanup();
+            for (AccountWithDataSet account : mAccounts) {
+                mAccountHelper.removeTestAccount(account);
+            }
         }
 
         @Test
-        public void findAccountsOfExistingContactsReturnsEmptyMapWhenNoMatchingContactsExist() {
+        public void returnsEmptyMapWhenNoMatchingContactsExist() {
+            mAccounts.add(mAccountHelper.addTestAccount());
+
             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));
+                    new SimContact(1, "Name 1 " + mNameSuffix, "5550101"),
+                    new SimContact(2, "Name 2 " + mNameSuffix, "5550102"),
+                    new SimContact(3, "Name 3 " + mNameSuffix, "5550103"),
+                    new SimContact(4, "Name 4 " + mNameSuffix, "5550104"));
 
             final Map<AccountWithDataSet, Set<SimContact>> existing = sut
                     .findAccountsOfExistingSimContacts(contacts);
@@ -245,16 +336,375 @@
             assertTrue(existing.isEmpty());
         }
 
+        @Test
+        public void hasAccountWithMatchingContactsWhenSingleMatchingContactExists()
+                throws Exception {
+            final SimContactDao sut = createDao();
+
+            final AccountWithDataSet account = mAccountHelper.addTestAccount(
+                    mAccountHelper.generateAccountName("primary_"));
+            mAccounts.add(account);
+
+            final SimContact existing1 =
+                    new SimContact(2, "Exists 2 " + mNameSuffix, "5550102");
+            final SimContact existing2 =
+                    new SimContact(4, "Exists 4 " + mNameSuffix, "5550104");
+
+            final List<SimContact> contacts = Arrays.asList(
+                    new SimContact(1, "Missing 1 " + mNameSuffix, "5550101"),
+                    new SimContact(existing1),
+                    new SimContact(3, "Missing 3 " + mNameSuffix, "5550103"),
+                    new SimContact(existing2));
+
+            sut.importContacts(Arrays.asList(
+                    new SimContact(existing1),
+                    new SimContact(existing2)
+            ), account);
+
+
+            final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+                    .findAccountsOfExistingSimContacts(contacts);
+
+            assertThat(existing.size(), equalTo(1));
+            assertThat(existing.get(account),
+                    Matchers.<Set<SimContact>>equalTo(ImmutableSet.of(existing1, existing2)));
+        }
+
+        @Test
+        public void hasMultipleAccountsWhenMultipleMatchingContactsExist() throws Exception {
+            final SimContactDao sut = createDao();
+
+            final AccountWithDataSet account1 = mAccountHelper.addTestAccount(
+                    mAccountHelper.generateAccountName("account1_"));
+            mAccounts.add(account1);
+            final AccountWithDataSet account2 = mAccountHelper.addTestAccount(
+                    mAccountHelper.generateAccountName("account2_"));
+            mAccounts.add(account2);
+
+            final SimContact existsInBoth =
+                    new SimContact(2, "Exists Both " + mNameSuffix, "5550102");
+            final SimContact existsInAccount1 =
+                    new SimContact(4, "Exists 1 " + mNameSuffix, "5550104");
+            final SimContact existsInAccount2 =
+                    new SimContact(5, "Exists 2 " + mNameSuffix, "5550105");
+
+            final List<SimContact> contacts = Arrays.asList(
+                    new SimContact(1, "Missing 1 " + mNameSuffix, "5550101"),
+                    new SimContact(existsInBoth),
+                    new SimContact(3, "Missing 3 " + mNameSuffix, "5550103"),
+                    new SimContact(existsInAccount1),
+                    new SimContact(existsInAccount2));
+
+            sut.importContacts(Arrays.asList(
+                    new SimContact(existsInBoth),
+                    new SimContact(existsInAccount1)
+            ), account1);
+
+            sut.importContacts(Arrays.asList(
+                    new SimContact(existsInBoth),
+                    new SimContact(existsInAccount2)
+            ), account2);
+
+
+            final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+                    .findAccountsOfExistingSimContacts(contacts);
+
+            assertThat(existing.size(), equalTo(2));
+            assertThat(existing, Matchers.<Map<AccountWithDataSet, Set<SimContact>>>equalTo(
+                    ImmutableMap.<AccountWithDataSet, Set<SimContact>>of(
+                            account1, ImmutableSet.of(existsInBoth, existsInAccount1),
+                            account2, ImmutableSet.of(existsInBoth, existsInAccount2))));
+        }
+
+        @Test
+        public void matchesByNameIfSimContactHasNoPhone() throws Exception {
+            final SimContactDao sut = createDao();
+
+            final AccountWithDataSet account = mAccountHelper.addTestAccount(
+                    mAccountHelper.generateAccountName("account_"));
+            mAccounts.add(account);
+
+            final SimContact noPhone = new SimContact(1, "Nophone " + mNameSuffix, null);
+            final SimContact otherExisting = new SimContact(
+                    5, "Exists 1 " + mNameSuffix, "5550105");
+
+            final List<SimContact> contacts = Arrays.asList(
+                    new SimContact(noPhone),
+                    new SimContact(2, "Name 2 " + mNameSuffix, "5550102"),
+                    new SimContact(3, "Name 3 " + mNameSuffix, "5550103"),
+                    new SimContact(4, "Name 4 " + mNameSuffix, "5550104"),
+                    new SimContact(otherExisting));
+
+            sut.importContacts(Arrays.asList(
+                    new SimContact(noPhone),
+                    new SimContact(otherExisting)
+            ), account);
+
+            final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+                    .findAccountsOfExistingSimContacts(contacts);
+
+            assertThat(existing.size(), equalTo(1));
+            assertThat(existing.get(account), Matchers.<Set<SimContact>>equalTo(
+                    ImmutableSet.of(noPhone, otherExisting)));
+        }
+
+        @Test
+        public void largeNumberOfSimContacts() throws Exception {
+            final SimContactDao sut = createDao();
+
+            final List<SimContact> contacts = new ArrayList<>();
+            for (int i = 0; i < MAX_SIM_CONTACTS; i++) {
+                contacts.add(new SimContact(
+                        i + 1, "Contact " + (i + 1) + " " + mNameSuffix, randomPhone()));
+            }
+            // The work has to be split into batches to avoid hitting SQL query parameter limits
+            // so test contacts that will be at boundary points
+            final SimContact imported1 = contacts.get(0);
+            final SimContact imported2 = contacts.get(99);
+            final SimContact imported3 = contacts.get(100);
+            final SimContact imported4 = contacts.get(101);
+            final SimContact imported5 = contacts.get(MAX_SIM_CONTACTS - 1);
+
+            final AccountWithDataSet account = mAccountHelper.addTestAccount(
+                    mAccountHelper.generateAccountName("account_"));
+            mAccounts.add(account);
+
+            sut.importContacts(Arrays.asList(imported1, imported2, imported3, imported4, imported5),
+                    account);
+
+            mAccounts.add(account);
+
+            final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+                    .findAccountsOfExistingSimContacts(contacts);
+
+            assertThat(existing.size(), equalTo(1));
+            assertThat(existing.get(account), Matchers.<Set<SimContact>>equalTo(
+                    ImmutableSet.of(imported1, imported2, imported3, imported4, imported5)));
+
+        }
+
         private SimContactDao createDao() {
             return SimContactDao.create(mContext);
         }
+
+        /**
+         * Adds a bunch of random contact data to CP2 to make the test environment more realistic
+         */
+        private static void seedCp2() throws RemoteException, OperationApplicationException {
+
+            final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+
+            appendCreateContact("John Smith", sSeedAccount, ops);
+            appendCreateContact("Marcus Seed", sSeedAccount, ops);
+            appendCreateContact("Gary Seed", sSeedAccount, ops);
+            appendCreateContact("Michael Seed", sSeedAccount, ops);
+            appendCreateContact("Isaac Seed", sSeedAccount, ops);
+            appendCreateContact("Sean Seed", sSeedAccount, ops);
+            appendCreateContact("Nate Seed", sSeedAccount, ops);
+            appendCreateContact("Andrey Seed", sSeedAccount, ops);
+            appendCreateContact("Cody Seed", sSeedAccount, ops);
+            appendCreateContact("John Seed", sSeedAccount, ops);
+            appendCreateContact("Alex Seed", sSeedAccount, ops);
+
+            InstrumentationRegistry.getTargetContext()
+                    .getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
+        }
+
+        private static void appendCreateContact(String name, AccountWithDataSet account,
+                ArrayList<ContentProviderOperation> ops) {
+            final int emailCount = sRandom.nextInt(10);
+            final int phoneCount = sRandom.nextInt(5);
+
+            final List<String> phones = new ArrayList<>();
+            for (int i = 0; i < phoneCount; i++) {
+                phones.add(randomPhone());
+            }
+            final List<String> emails = new ArrayList<>();
+            for (int i = 0; i < emailCount; i++) {
+                emails.add(randomEmail(name));
+            }
+            appendCreateContact(name, phones, emails, account, ops);
+        }
+
+
+        private static void appendCreateContact(String name, List<String> phoneNumbers,
+                List<String> emails, AccountWithDataSet account, List<ContentProviderOperation> ops) {
+            int index = ops.size();
+
+            ops.add(account.newRawContactOperation());
+            ops.add(insertIntoData(name, StructuredName.CONTENT_ITEM_TYPE, index));
+            for (String phone : phoneNumbers) {
+                ops.add(insertIntoData(phone, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_MOBILE, index));
+            }
+            for (String email : emails) {
+                ops.add(insertIntoData(email, Email.CONTENT_ITEM_TYPE, Email.TYPE_HOME, index));
+            }
+        }
+
+        private static ContentProviderOperation insertIntoData(String value, String mimeType,
+                int idBackReference) {
+            return ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                    .withValue(Data.DATA1, value)
+                    .withValue(Data.MIMETYPE, mimeType)
+                    .withValueBackReference(Data.RAW_CONTACT_ID, idBackReference).build();
+        }
+
+        private static ContentProviderOperation insertIntoData(String value, String mimeType,
+                int type, int idBackReference) {
+            return ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                    .withValue(Data.DATA1, value)
+                    .withValue(ContactsContract.Data.DATA2, type)
+                    .withValue(Data.MIMETYPE, mimeType)
+                    .withValueBackReference(Data.RAW_CONTACT_ID, idBackReference).build();
+        }
+    }
+
+    /**
+     * Tests for {@link SimContactDao#loadContactsForSim(SimCard)}
+     *
+     * These are unit tests that verify that {@link SimContact}s are created correctly from
+     * the cursors that are returned by queries to the IccProvider
+     */
+    @SmallTest
+    @RunWith(AndroidJUnit4.class)
+    public static class LoadContactsUnitTests {
+
+        private MockContentProvider mMockIccProvider;
+        private Context mContext;
+
+        @Before
+        public void setUp() {
+            mContext = mock(MockContext.class);
+            final MockContentResolver mockResolver = new MockContentResolver();
+            mMockIccProvider = new MockContentProvider();
+            mockResolver.addProvider("icc", mMockIccProvider);
+            when(mContext.getContentResolver()).thenReturn(mockResolver);
+        }
+
+
+        @Test
+        public void createsContactsFromCursor() {
+            mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+                    .withDefaultProjection(
+                            SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+                            SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+                    .withAnyProjection()
+                    .withAnySelection()
+                    .withAnySortOrder()
+                    .returnRow(1, "Name One", "5550101", null)
+                    .returnRow(2, "Name Two", "5550102", null)
+                    .returnRow(3, "Name Three", null, null)
+                    .returnRow(4, null, "5550104", null)
+                    .returnRow(5, "Name Five", "5550105",
+                            "five@example.com,nf@example.com,name.five@example.com")
+                    .returnRow(6, "Name Six", "5550106", "thesix@example.com");
+
+            final SimContactDao sut = SimContactDao.create(mContext);
+            final List<SimContact> contacts = sut
+                    .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+
+            assertThat(contacts, equalTo(
+                    Arrays.asList(
+                            new SimContact(1, "Name One", "5550101", null),
+                            new SimContact(2, "Name Two", "5550102", null),
+                            new SimContact(3, "Name Three", null, null),
+                            new SimContact(4, null, "5550104", null),
+                            new SimContact(5, "Name Five", "5550105", new String[] {
+                                    "five@example.com", "nf@example.com", "name.five@example.com"
+                            }),
+                            new SimContact(6, "Name Six", "5550106", new String[] {
+                                    "thesix@example.com"
+                            })
+                    )));
+        }
+
+        @Test
+        public void excludesEmptyContactsFromResult() {
+            mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+                    .withDefaultProjection(
+                            SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+                            SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+                    .withAnyProjection()
+                    .withAnySelection()
+                    .withAnySortOrder()
+                    .returnRow(1, "Non Empty1", "5550101", null)
+                    .returnRow(2, "", "", "")
+                    .returnRow(3, "Non Empty2", null, null)
+                    .returnRow(4, null, null, null)
+                    .returnRow(5, "", null, null)
+                    .returnRow(6, null, "5550102", null)
+                    .returnRow(7, null, null, "user@example.com");
+
+            final SimContactDao sut = SimContactDao.create(mContext);
+            final List<SimContact> contacts = sut
+                    .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+
+            assertThat(contacts, equalTo(
+                    Arrays.asList(
+                            new SimContact(1, "Non Empty1", "5550101", null),
+                            new SimContact(3, "Non Empty2", null, null),
+                            new SimContact(6, null, "5550102", null),
+                            new SimContact(7, null, null, new String[] { "user@example.com" })
+                    )));
+        }
+
+        @Test
+        public void usesSimCardSubscriptionIdIfAvailable() {
+            mMockIccProvider.expectQuery(SimContactDaoImpl.ICC_CONTENT_URI.buildUpon()
+                    .appendPath("subId").appendPath("2").build())
+                    .withDefaultProjection(
+                            SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+                            SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+                    .withAnyProjection()
+                    .withAnySelection()
+                    .withAnySortOrder()
+                    .returnEmptyCursor();
+
+            final SimContactDao sut = SimContactDao.create(mContext);
+            sut.loadContactsForSim(new SimCard("123", 2, "carrier", "sim", null, "us"));
+            mMockIccProvider.verify();
+        }
+
+        @Test
+        public void omitsSimCardSubscriptionIdIfUnavailable() {
+            mMockIccProvider.expectQuery(SimContactDaoImpl.ICC_CONTENT_URI)
+                    .withDefaultProjection(
+                            SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+                            SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+                    .withAnyProjection()
+                    .withAnySelection()
+                    .withAnySortOrder()
+                    .returnEmptyCursor();
+
+            final SimContactDao sut = SimContactDao.create(mContext);
+            sut.loadContactsForSim(new SimCard("123", SimCard.NO_SUBSCRIPTION_ID,
+                    "carrier", "sim", null, "us"));
+            mMockIccProvider.verify();
+        }
+
+        @Test
+        public void returnsEmptyListForEmptyCursor() {
+            mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+                    .withDefaultProjection(
+                            SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+                            SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+                    .withAnyProjection()
+                    .withAnySelection()
+                    .withAnySortOrder()
+                    .returnEmptyCursor();
+
+            final SimContactDao sut = SimContactDao.create(mContext);
+            List<SimContact> result = sut
+                    .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+            assertTrue(result.isEmpty());
+        }
     }
 
     @LargeTest
     // suppressed because failed assumptions are reported as test failures by the build server
     @Suppress
     @RunWith(AndroidJUnit4.class)
-    public static class ReadIntegrationTest {
+    public static class LoadContactsIntegrationTest {
         private SimContactsTestHelper mSimTestHelper;
         private ArrayList<ContentProviderOperation> mSimSnapshot;
 
@@ -281,7 +731,8 @@
             mSimTestHelper.addSimContact("Test Simthree", "15095550103");
 
             final SimContactDao sut = SimContactDao.create(getContext());
-            final ArrayList<SimContact> contacts = sut.loadSimContacts();
+            final SimCard sim = sut.getSimCards().get(0);
+            final ArrayList<SimContact> contacts = sut.loadContactsForSim(sim);
 
             assertThat(contacts.get(0), isSimContactWithNameAndPhone("Test Simone", "15095550101"));
             assertThat(contacts.get(1), isSimContactWithNameAndPhone("Test Simtwo", "15095550102"));
@@ -327,7 +778,7 @@
     }
 
     private static Matcher<Cursor> hasMimeType(String type) {
-        return hasValueForColumn(ContactsContract.Data.MIMETYPE, type);
+        return hasValueForColumn(Data.MIMETYPE, type);
     }
 
     private static Matcher<Cursor> hasValueForColumn(final String column, final String value) {
@@ -374,23 +825,23 @@
 
     private static Matcher<Cursor> hasName(final String name) {
         return hasRowMatching(allOf(
-                hasMimeType(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE),
+                hasMimeType(StructuredName.CONTENT_ITEM_TYPE),
                 hasValueForColumn(
-                        ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)));
+                        StructuredName.DISPLAY_NAME, name)));
     }
 
     private static Matcher<Cursor> hasPhone(final String phone) {
         return hasRowMatching(allOf(
-                hasMimeType(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE),
+                hasMimeType(Phone.CONTENT_ITEM_TYPE),
                 hasValueForColumn(
-                        ContactsContract.CommonDataKinds.Phone.NUMBER, phone)));
+                        Phone.NUMBER, phone)));
     }
 
     private static Matcher<Cursor> hasEmail(final String email) {
         return hasRowMatching(allOf(
-                hasMimeType(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE),
+                hasMimeType(Email.CONTENT_ITEM_TYPE),
                 hasValueForColumn(
-                        ContactsContract.CommonDataKinds.Email.ADDRESS, email)));
+                        Email.ADDRESS, email)));
     }
 
     static class StringableCursor extends CursorWrapper {
@@ -410,7 +861,19 @@
         }
     }
 
+    private static String randomPhone() {
+        return String.format(Locale.US, "1%s55501%02d",
+                AREA_CODES[sRandom.nextInt(AREA_CODES.length)],
+                sRandom.nextInt(100));
+    }
+
+    private static String randomEmail(String name) {
+        return String.format("%s%d@example.com", name.replace(" ", ".").toLowerCase(Locale.US),
+                1000 + sRandom.nextInt(1000));
+    }
+
+
     static Context getContext() {
         return InstrumentationRegistry.getTargetContext();
-    }
+   }
 }
diff --git a/tests/src/com/android/contacts/tests/AccountsTestHelper.java b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
index 123d538..15cec3c 100644
--- a/tests/src/com/android/contacts/tests/AccountsTestHelper.java
+++ b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
@@ -17,6 +17,7 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Build;
@@ -27,6 +28,11 @@
 
 import com.android.contacts.common.model.account.AccountWithDataSet;
 
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
@@ -80,12 +86,27 @@
         mAccountManager.removeAccountExplicitly(remove);
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
+    public void removeTestAccount(String accountName) {
+        final Account remove = new Account(accountName, TEST_ACCOUNT_TYPE);
+        mAccountManager.removeAccountExplicitly(remove);
+    }
+
+    public boolean hasTestAccount(String name) {
+        final List<Account> accounts = Arrays.asList(
+                mAccountManager.getAccountsByType(TEST_ACCOUNT_TYPE));
+        return accounts.contains(new Account(name, TEST_ACCOUNT_TYPE));
+    }
+
     public void removeContactsForAccount() {
-        // Not sure if this is necessary or if contacts are automatically cleaned up when the
-        // account is removed.
+        removeContactsForAccount(
+                new AccountWithDataSet(mTestAccount.name, mTestAccount.type, null));
+    }
+
+    public void removeContactsForAccount(AccountWithDataSet account) {
         mResolver.delete(RawContacts.CONTENT_URI,
                 RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
-                new String[] { mTestAccount.name, mTestAccount.type });
+                new String[] { account.name, account.type });
     }
 
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
diff --git a/tests/src/com/android/contacts/tests/AdbHelpers.java b/tests/src/com/android/contacts/tests/AdbHelpers.java
index 163d7cc..cf66cda 100644
--- a/tests/src/com/android/contacts/tests/AdbHelpers.java
+++ b/tests/src/com/android/contacts/tests/AdbHelpers.java
@@ -16,8 +16,10 @@
 package com.android.contacts.tests;
 
 import android.content.Context;
+import android.content.OperationApplicationException;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.support.annotation.RequiresApi;
 import android.support.test.InstrumentationRegistry;
 import android.util.Log;
@@ -85,6 +87,11 @@
                 getAppContext().getPackageName(), Context.MODE_PRIVATE).getAll());
     }
 
+    public static void clearSimCard(Context context)
+            throws RemoteException, OperationApplicationException {
+        new SimContactsTestHelper(context).deleteAllSimContacts();
+    }
+
     private static Context getAppContext() {
         return InstrumentationRegistry.getTargetContext();
     }
diff --git a/tests/src/com/android/contacts/tests/FakeSimContactDao.java b/tests/src/com/android/contacts/tests/FakeSimContactDao.java
new file mode 100644
index 0000000..ba091ca
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/FakeSimContactDao.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package com.android.contacts.tests;
+
+import android.content.ContentProviderResult;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Fake implementation of SimContactDao for testing
+ */
+public class FakeSimContactDao extends SimContactDao {
+
+    public boolean canReadSimContacts = true;
+    public List<SimCard> simCards;
+    public Map<SimCard, ArrayList<SimContact>> simContacts;
+    public ContentProviderResult[] importResult;
+    public Map<AccountWithDataSet, Set<SimContact>> existingSimContacts;
+
+    public FakeSimContactDao() {
+        simCards = new ArrayList<>();
+        simContacts = new HashMap<>();
+        importResult = new ContentProviderResult[0];
+        existingSimContacts = new HashMap<>();
+    }
+
+    @Override
+    public boolean canReadSimContacts() {
+        return canReadSimContacts;
+    }
+
+    @Override
+    public List<SimCard> getSimCards() {
+        return simCards;
+    }
+
+    @Override
+    public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
+        return simContacts.get(sim);
+    }
+
+    @Override
+    public ContentProviderResult[] importContacts(List<SimContact> contacts,
+            AccountWithDataSet targetAccount)
+            throws RemoteException, OperationApplicationException {
+        return importResult;
+    }
+
+    @Override
+    public void persistSimStates(List<SimCard> simCards) {
+        this.simCards = simCards;
+    }
+
+    @Override
+    public SimCard getSimBySubscriptionId(int subscriptionId) {
+        for (SimCard sim : simCards) {
+            if (sim.getSubscriptionId() == subscriptionId) {
+                return sim;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
+            List<SimContact> contacts) {
+        return existingSimContacts;
+    }
+
+    public FakeSimContactDao addSim(SimCard sim, SimContact... contacts) {
+        simCards.add(sim);
+        simContacts.put(sim, new ArrayList<>(Arrays.asList(contacts)));
+        return this;
+    }
+
+    public static FakeSimContactDao singleSimWithContacts(SimCard sim, SimContact... contacts) {
+        return new FakeSimContactDao().addSim(sim, contacts);
+    }
+
+    public static FakeSimContactDao noSim() {
+        FakeSimContactDao result = new FakeSimContactDao();
+        result.canReadSimContacts = false;
+        return result;
+    }
+
+}
diff --git a/tests/src/com/android/contacts/tests/SimContactsTestHelper.java b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
index c2ffead..552d790 100644
--- a/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
+++ b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
@@ -15,7 +15,6 @@
  */
 package com.android.contacts.tests;
 
-import android.content.ContentProvider;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
@@ -29,9 +28,10 @@
 import android.support.test.InstrumentationRegistry;
 import android.telephony.TelephonyManager;
 
+import com.android.contacts.common.database.SimContactDaoImpl;
+import com.android.contacts.common.model.SimCard;
 import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.database.SimContactDao;
-import com.android.contacts.common.test.mocks.MockContentProvider;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,7 +59,7 @@
     }
 
     public int getSimContactCount() {
-        Cursor cursor = mContext.getContentResolver().query(SimContactDao.ICC_CONTENT_URI,
+        Cursor cursor = mContext.getContentResolver().query(SimContactDaoImpl.ICC_CONTENT_URI,
                 null, null, null, null);
         try {
             return cursor.getCount();
@@ -68,32 +68,6 @@
         }
     }
 
-    public ContentValues iccRow(long id, String name, String number, String emails) {
-        ContentValues values = new ContentValues();
-        values.put(SimContactDao._ID, id);
-        values.put(SimContactDao.NAME, name);
-        values.put(SimContactDao.NUMBER, number);
-        values.put(SimContactDao.EMAILS, emails);
-        return values;
-    }
-
-    public ContentProvider iccProviderExpectingNoQueries() {
-        return new MockContentProvider();
-    }
-
-    public ContentProvider emptyIccProvider() {
-        final MockContentProvider provider = new MockContentProvider();
-        provider.expectQuery(SimContactDao.ICC_CONTENT_URI)
-                .withDefaultProjection(
-                        SimContactDao._ID, SimContactDao.NAME,
-                        SimContactDao.NUMBER, SimContactDao.EMAILS)
-                .withAnyProjection()
-                .withAnySelection()
-                .withAnySortOrder()
-                .returnEmptyCursor();
-        return provider;
-    }
-
     public Uri addSimContact(String name, String number) {
         ContentValues values = new ContentValues();
         // Oddly even though it's called name when querying we have to use "tag" for it to work
@@ -102,23 +76,26 @@
             values.put("tag", name);
         }
         if (number != null) {
-            values.put(SimContactDao.NUMBER, number);
+            values.put(SimContactDaoImpl.NUMBER, number);
         }
-        return mResolver.insert(SimContactDao.ICC_CONTENT_URI, values);
+        return mResolver.insert(SimContactDaoImpl.ICC_CONTENT_URI, values);
     }
 
     public ContentProviderResult[] deleteAllSimContacts()
             throws RemoteException, OperationApplicationException {
-        SimContactDao dao = SimContactDao.create(mContext);
-        List<SimContact> contacts = dao.loadSimContacts();
+        final List<SimCard> sims = mSimDao.getSimCards();
+        if (sims.isEmpty()) {
+            throw new IllegalStateException("Expected SIM card");
+        }
+        final List<SimContact> contacts = mSimDao.loadContactsForSim(sims.get(0));
         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
         for (SimContact contact : contacts) {
             ops.add(ContentProviderOperation
-                    .newDelete(SimContactDao.ICC_CONTENT_URI)
+                    .newDelete(SimContactDaoImpl.ICC_CONTENT_URI)
                     .withSelection(getWriteSelection(contact), null)
                     .build());
         }
-        return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), ops);
+        return mResolver.applyBatch(SimContactDaoImpl.ICC_CONTENT_URI.getAuthority(), ops);
     }
 
     public ContentProviderResult[] restore(ArrayList<ContentProviderOperation> restoreOps)
@@ -128,13 +105,17 @@
         // Remove SIM contacts because we assume that caller wants the data to be in the exact
         // state as when the restore ops were captured.
         deleteAllSimContacts();
-        return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), restoreOps);
+        return mResolver.applyBatch(SimContactDaoImpl.ICC_CONTENT_URI.getAuthority(), restoreOps);
     }
 
     public ArrayList<ContentProviderOperation> captureRestoreSnapshot() {
-        ArrayList<SimContact> contacts = mSimDao.loadSimContacts();
+        final List<SimCard> sims = mSimDao.getSimCards();
+        if (sims.isEmpty()) {
+            throw new IllegalStateException("Expected SIM card");
+        }
+        final ArrayList<SimContact> contacts = mSimDao.loadContactsForSim(sims.get(0));
 
-        ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+        final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
         for (SimContact contact : contacts) {
             final String[] emails = contact.getEmails();
             if (emails != null && emails.length > 0) {
@@ -142,7 +123,7 @@
                         " Please manually remove SIM contacts with emails.");
             }
             ops.add(ContentProviderOperation
-                    .newInsert(SimContactDao.ICC_CONTENT_URI)
+                    .newInsert(SimContactDaoImpl.ICC_CONTENT_URI)
                     .withValue("tag", contact.getName())
                     .withValue("number", contact.getPhone())
                     .build());
@@ -151,15 +132,15 @@
     }
 
     public String getWriteSelection(SimContact simContact) {
-        return "tag='" + simContact.getName() + "' AND " + SimContactDao.NUMBER + "='" +
+        return "tag='" + simContact.getName() + "' AND " + SimContactDaoImpl.NUMBER + "='" +
                 simContact.getPhone() + "'";
     }
 
     public int deleteSimContact(@NonNull  String name, @NonNull  String number) {
         // IccProvider doesn't use the selection args.
         final String selection = "tag='" + name + "' AND " +
-                SimContactDao.NUMBER + "='" + number + "'";
-        return mResolver.delete(SimContactDao.ICC_CONTENT_URI, selection, null);
+                SimContactDaoImpl.NUMBER + "='" + number + "'";
+        return mResolver.delete(SimContactDaoImpl.ICC_CONTENT_URI, selection, null);
     }
 
     public boolean isSimReady() {