DO NOT MERGE Improve testability of SIM import code.

For Iec6eb441fe197daceb24e87288f8e0c5ac0ce2cf

test
ran GoogleContactsTests

Change-Id: I7d34e68f389143f94c26190cd9b3206ca871c64e
(cherry picked from commit 52d8cae888457d9639696a19db29a2ff36deca85)
diff --git a/src/com/android/contacts/SimImportService.java b/src/com/android/contacts/SimImportService.java
index 7a4997e..4fa3695 100644
--- a/src/com/android/contacts/SimImportService.java
+++ b/src/com/android/contacts/SimImportService.java
@@ -32,6 +32,7 @@
 
 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;
@@ -49,6 +50,26 @@
 
     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";
@@ -74,12 +95,24 @@
     // 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>
      */
-    public static boolean isImporting(SimCard sim) {
+    private static boolean isImporting(SimCard sim) {
         for (ImportTask task : sPending) {
             if (task.getSim().equals(sim)) {
                 return true;
@@ -88,13 +121,8 @@
         return false;
     }
 
-    /**
-     * Returns whether there is any imports still pending
-     *
-     * <p>This should be called from the UI thread</p>
-     */
-    public static boolean isRunning() {
-        return !sPending.isEmpty();
+    public static StatusProvider getStatusProvider() {
+        return sStatusProvider;
     }
 
     /**
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..a6824bb
--- /dev/null
+++ b/src/com/android/contacts/common/database/SimContactDaoImpl.java
@@ -0,0 +1,473 @@
+/*
+ * 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));
+            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);
+    }
+
+    // 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/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
index ec03f0f..e180ca2 100644
--- a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -30,6 +30,7 @@
 import android.support.test.filters.Suppress;
 import android.support.test.runner.AndroidJUnit4;
 
+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.tests.AccountsTestHelper;
@@ -281,7 +282,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"));
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() {