Add additional tests of SIM contact importing

Test: ran GoogleContacts unit tests. Also manually verified that SIM
import screen functionality is the same

Change-Id: I21bf392c0e4a12171f73f7f676cadf33183efb4f
diff --git a/src/com/android/contacts/common/database/SimContactDaoImpl.java b/src/com/android/contacts/common/database/SimContactDaoImpl.java
index a6824bb..6ddf663 100644
--- a/src/com/android/contacts/common/database/SimContactDaoImpl.java
+++ b/src/com/android/contacts/common/database/SimContactDaoImpl.java
@@ -293,7 +293,10 @@
             final String emails = cursor.getString(colEmails);
 
             final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
-            result.add(contact);
+            // Only include contact if it has some useful data
+            if (contact.hasName() || contact.hasPhone() || contact.hasEmails()) {
+                result.add(contact);
+            }
         }
         return result;
     }
@@ -381,7 +384,7 @@
     }
 
     private String[] parseEmails(String emails) {
-        return emails != null ? emails.split(",") : null;
+        return !TextUtils.isEmpty(emails) ? emails.split(",") : null;
     }
 
     private boolean hasTelephony() {
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/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
index e180ca2..4702442 100644
--- a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -18,29 +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;
@@ -48,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
@@ -90,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"
                     })
@@ -170,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 " +
@@ -181,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[] {
@@ -191,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,
@@ -203,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);
@@ -246,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;
 
@@ -329,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) {
@@ -376,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 {
@@ -412,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();
-    }
+   }
 }