Adding SimPhonebookProvider

This is the provider that implements SimPhonebookContract.

Test: atest TeleServiceTests:SimPhonebookProviderTest
Bug: 154363919
Change-Id: I584150dd8352dbb49d70ea28c6d064a6336d018d
diff --git a/tests/Android.bp b/tests/Android.bp
index 13c0dc8..4eacf6d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -33,9 +33,13 @@
     instrumentation_for: "TeleService",
 
     static_libs: [
+        "androidx.test.core",
+        "androidx.test.espresso.core",
+        "androidx.test.ext.junit",
         "androidx.test.rules",
         "mockito-target-minus-junit4",
-        "androidx.test.espresso.core",
+        "telephony-common-testing",
+        "testng",
         "truth-prebuilt",
 	"testables",
     ],
@@ -46,3 +50,4 @@
     ],
 
 }
+
diff --git a/tests/src/com/android/phone/SimPhonebookProviderTest.java b/tests/src/com/android/phone/SimPhonebookProviderTest.java
new file mode 100644
index 0000000..1d48694
--- /dev/null
+++ b/tests/src/com/android/phone/SimPhonebookProviderTest.java
@@ -0,0 +1,1490 @@
+/*
+ * Copyright (C) 2020 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.phone;
+
+import static android.provider.SimPhonebookContract.ElementaryFiles.EF_ADN;
+import static android.provider.SimPhonebookContract.ElementaryFiles.EF_FDN;
+import static android.provider.SimPhonebookContract.ElementaryFiles.EF_SDN;
+
+import static com.android.internal.telephony.testing.CursorSubject.assertThat;
+import static com.android.internal.telephony.testing.TelephonyAssertions.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.SimPhonebookContract;
+import android.provider.SimPhonebookContract.ElementaryFiles;
+import android.provider.SimPhonebookContract.SimRecords;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.util.Pair;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.provider.ProviderTestRule;
+
+import com.android.internal.telephony.IIccPhoneBook;
+import com.android.internal.telephony.uicc.AdnRecord;
+import com.android.internal.telephony.uicc.IccConstants;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public final class SimPhonebookProviderTest {
+
+    // Emojis aren't currently supported for the ADN record label.
+    private static final String EMOJI = new String(Character.toChars(0x1F642));
+    private static final String UNSUPPORTED_NAME = ":)=" + EMOJI + ";ni=日;hon=本;";
+    private static final String UNSUPPORTED_NAME2 = "日本" + EMOJI;
+    private static final Correspondence<AdnRecord, AdnRecord> ADN_RECORD_IS_EQUAL =
+            Correspondence.from(AdnRecord::isEqual, "isEqual");
+
+    @Rule
+    public final ProviderTestRule mProviderRule = new ProviderTestRule.Builder(
+            TestableSimPhonebookProvider.class, SimPhonebookContract.AUTHORITY).build();
+
+    private ContentResolver mResolver;
+    private FakeIccPhoneBook mIccPhoneBook;
+    private SubscriptionManager mMockSubscriptionManager;
+
+    private static List<SubscriptionInfo> createSubscriptionsWithIds(int... subscriptionIds) {
+        ImmutableList.Builder<SubscriptionInfo> builder = ImmutableList.builderWithExpectedSize(
+                subscriptionIds.length);
+        for (int i = 0; i < subscriptionIds.length; i++) {
+            builder.add(createSubscriptionInfo(i, subscriptionIds[i]));
+        }
+        return builder.build();
+    }
+
+    private static SubscriptionInfo createSubscriptionInfo(int slotIndex, int subscriptiondId) {
+        return new SubscriptionInfo(
+                subscriptiondId, "", slotIndex, null, null, 0, 0, null, 0, null, null, null, null,
+                false, null, null);
+    }
+
+    @Before
+    public void setUp() {
+        mMockSubscriptionManager = spy(
+                Objects.requireNonNull(ApplicationProvider.getApplicationContext()
+                        .getSystemService(SubscriptionManager.class)));
+        mIccPhoneBook = new FakeIccPhoneBook();
+        mResolver = mProviderRule.getResolver();
+
+        TestableSimPhonebookProvider.setup(mResolver, mMockSubscriptionManager, mIccPhoneBook);
+    }
+
+    @Test
+    public void query_entityFiles_returnsCursorWithCorrectProjection() {
+        // Empty projection
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, new String[0], null,
+                null)) {
+            assertThat(cursor).hasColumnNames();
+        }
+
+        // Single column
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, new String[]{
+                ElementaryFiles.EF_TYPE
+        }, null, null)) {
+            assertThat(cursor).hasColumnNames(ElementaryFiles.EF_TYPE);
+        }
+
+        // Duplicate column
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, new String[]{
+                ElementaryFiles.SUBSCRIPTION_ID, ElementaryFiles.SUBSCRIPTION_ID
+        }, null, null)) {
+            assertThat(cursor).hasColumnNames(ElementaryFiles.SUBSCRIPTION_ID,
+                    ElementaryFiles.SUBSCRIPTION_ID);
+        }
+
+        // Random order of all columns
+        String[] projection = Arrays.copyOf(
+                SimPhonebookProvider.ELEMENTARY_FILES_ALL_COLUMNS,
+                SimPhonebookProvider.ELEMENTARY_FILES_ALL_COLUMNS.length);
+        Collections.shuffle(Arrays.asList(projection));
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, projection, null, null)) {
+            assertThat(cursor).hasColumnNames(projection);
+        }
+    }
+
+    @Test
+    public void query_entityFiles_unrecognizedColumn_throwsIllegalArgumentException() {
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(ElementaryFiles.CONTENT_URI, new String[]{"invalid_column"}, null,
+                        null));
+    }
+
+    @Test
+    public void query_entityFiles_noSim_returnsEmptyCursor() {
+        when(mMockSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(
+                ImmutableList.of());
+
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, null, null, null)) {
+            assertThat(cursor).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_entityFiles_multiSim_returnsCursorWithRowForEachSimEf() {
+        setupSimsWithSubscriptionIds(2, 3, 7);
+
+        mIccPhoneBook.setRecordsSize(2, IccConstants.EF_ADN, 10, 25);
+        mIccPhoneBook.setRecordsSize(2, IccConstants.EF_FDN, 5, 20);
+        mIccPhoneBook.setRecordsSize(2, IccConstants.EF_SDN, 15, 20);
+        mIccPhoneBook.setRecordsSize(3, IccConstants.EF_ADN, 100, 30);
+        // These Will be omitted from results because zero size indicates the EF is not supported.
+        mIccPhoneBook.setRecordsSize(3, IccConstants.EF_FDN, 0, 0);
+        mIccPhoneBook.setRecordsSize(3, IccConstants.EF_SDN, 0, 0);
+        mIccPhoneBook.setRecordsSize(7, IccConstants.EF_ADN, 0, 0);
+        mIccPhoneBook.setRecordsSize(7, IccConstants.EF_FDN, 0, 0);
+        mIccPhoneBook.setRecordsSize(7, IccConstants.EF_SDN, 0, 0);
+
+        String[] projection = {
+                ElementaryFiles.SLOT_INDEX, ElementaryFiles.SUBSCRIPTION_ID,
+                ElementaryFiles.EF_TYPE, ElementaryFiles.MAX_RECORDS,
+                ElementaryFiles.NAME_MAX_LENGTH, ElementaryFiles.PHONE_NUMBER_MAX_LENGTH
+        };
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, projection, null, null)) {
+            assertThat(cursor).hasColumnNames(projection);
+
+            assertThat(cursor)
+                    .atRow(0).hasRowValues(0, 2, ElementaryFiles.EF_ADN, 10, 11, 20)
+                    .atRow(1).hasRowValues(0, 2, ElementaryFiles.EF_FDN, 5, 6, 20)
+                    .atRow(2).hasRowValues(0, 2, ElementaryFiles.EF_SDN, 15, 6, 20)
+                    .atRow(3).hasRowValues(1, 3, ElementaryFiles.EF_ADN, 100, 16, 20);
+        }
+    }
+
+    @Test
+    public void query_entityFiles_simWithZeroSizes_returnsEmptyCursor() {
+        setupSimsWithSubscriptionIds(1);
+
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 0, 0);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_FDN, 0, 0);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_SDN, 0, 0);
+
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, null, null, null)) {
+            assertThat(cursor).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_adnRecords_returnsCursorWithMatchingProjection() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        Uri contentAdn = SimRecords.getContentUri(1, EF_ADN);
+
+        // Empty projection
+        try (Cursor cursor = mResolver.query(contentAdn, new String[0], null, null)) {
+            assertThat(cursor).hasColumnNames();
+        }
+
+        // Single column
+        try (Cursor cursor = mResolver.query(contentAdn, new String[] {
+                SimRecords.PHONE_NUMBER
+        }, null, null)) {
+            assertThat(cursor).hasColumnNames(SimRecords.PHONE_NUMBER);
+        }
+
+        // Duplicate column
+        try (Cursor cursor = mResolver.query(contentAdn, new String[]{
+                SimRecords.PHONE_NUMBER, SimRecords.PHONE_NUMBER
+        }, null, null)) {
+            assertThat(cursor).hasColumnNames(SimRecords.PHONE_NUMBER, SimRecords.PHONE_NUMBER);
+        }
+
+        // Random order of all columns
+        String[] projection = Arrays.copyOf(
+                SimPhonebookProvider.SIM_RECORDS_ALL_COLUMNS,
+                SimPhonebookProvider.SIM_RECORDS_ALL_COLUMNS.length);
+        Collections.shuffle(Arrays.asList(projection));
+        try (Cursor cursor = mResolver.query(contentAdn, projection, null, null)) {
+            assertThat(cursor).hasColumnNames(projection);
+        }
+    }
+
+    @Test
+    public void query_adnRecords_noRecords_returnsEmptyCursor() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        try (Cursor cursor = mResolver.query(SimRecords.getContentUri(1, EF_ADN), null, null,
+                null)) {
+            assertThat(cursor).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_simRecords_nullRecordList_returnsEmptyCursor() throws Exception {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        // Use a mock so that a null list can be returned
+        IIccPhoneBook mockIccPhoneBook = mock(
+                IIccPhoneBook.class, AdditionalAnswers.delegatesTo(mIccPhoneBook));
+        when(mockIccPhoneBook.getAdnRecordsInEfForSubscriber(anyInt(), anyInt())).thenReturn(null);
+        TestableSimPhonebookProvider.setup(mResolver, mMockSubscriptionManager, mockIccPhoneBook);
+
+        try (Cursor adnCursor = mResolver.query(SimRecords.getContentUri(1, EF_ADN), null, null,
+                null);
+             Cursor fdnCursor = mResolver.query(SimRecords.getContentUri(1, EF_FDN), null, null,
+                     null);
+             Cursor sdnCursor = mResolver.query(SimRecords.getContentUri(1, EF_SDN), null, null,
+                     null)
+        ) {
+            assertThat(adnCursor).hasCount(0);
+            assertThat(fdnCursor).hasCount(0);
+            assertThat(sdnCursor).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_simRecords_singleSim_returnsDataForCorrectEf() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Person Adn1", "8005550101");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Person Adn2", "8005550102");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_FDN, "Person Fdn", "8005550103");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_SDN, "Person Sdn", "8005550104");
+        mIccPhoneBook.setDefaultSubscriptionId(1);
+
+        String[] projection = {
+                SimRecords.SUBSCRIPTION_ID,
+                SimRecords.ELEMENTARY_FILE_TYPE,
+                SimRecords.RECORD_NUMBER,
+                SimRecords.NAME,
+                SimRecords.PHONE_NUMBER
+        };
+        try (Cursor adnCursor = mResolver.query(SimRecords.getContentUri(1, EF_ADN),
+                projection, null, null);
+             Cursor fdnCursor = mResolver.query(SimRecords.getContentUri(1, EF_FDN),
+                     projection, null, null);
+             Cursor sdnCursor = mResolver.query(SimRecords.getContentUri(1, EF_SDN),
+                     projection, null, null)
+        ) {
+
+            assertThat(adnCursor)
+                    .atRow(0).hasRowValues(1, ElementaryFiles.EF_ADN, 1, "Person Adn1",
+                    "8005550101")
+                    .atRow(1).hasRowValues(1, ElementaryFiles.EF_ADN, 2, "Person Adn2",
+                    "8005550102");
+            assertThat(fdnCursor)
+                    .atRow(0).hasRowValues(1, ElementaryFiles.EF_FDN, 1, "Person Fdn",
+                    "8005550103");
+            assertThat(sdnCursor)
+                    .atRow(0).hasRowValues(1, ElementaryFiles.EF_SDN, 1, "Person Sdn",
+                    "8005550104");
+        }
+    }
+
+    @Test
+    public void query_adnRecords_returnsAdnData() {
+        setupSimsWithSubscriptionIds(1, 2, 4);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Person Sim1", "8005550101");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_FDN, "Omitted Sim1", "8005550199");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_ADN, "Person Sim2a", "8005550103");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_ADN, "Person Sim2b", "8005550104");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_ADN, "Person Sim2c", "8005550105");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_SDN, "Omitted Sim2", "8005550198");
+        mIccPhoneBook.addRecord(4, IccConstants.EF_ADN, "Person Sim4", "8005550106");
+        mIccPhoneBook.setDefaultSubscriptionId(1);
+
+        String[] projection = {
+                SimRecords.SUBSCRIPTION_ID,
+                SimRecords.ELEMENTARY_FILE_TYPE,
+                SimRecords.RECORD_NUMBER,
+                SimRecords.NAME,
+                SimRecords.PHONE_NUMBER
+        };
+        try (Cursor cursorSim1 = mResolver.query(SimRecords.getContentUri(1, EF_ADN),
+                projection, null, null);
+             Cursor cursorSim2 = mResolver.query(SimRecords.getContentUri(2, EF_ADN),
+                     projection, null, null);
+             Cursor cursorSim4 = mResolver.query(SimRecords.getContentUri(4, EF_ADN),
+                     projection, null, null)
+        ) {
+
+            assertThat(cursorSim1).hasData(new Object[][]{
+                    {1, ElementaryFiles.EF_ADN, 1, "Person Sim1", "8005550101"},
+            });
+            assertThat(cursorSim2).hasData(new Object[][]{
+                    {2, ElementaryFiles.EF_ADN, 1, "Person Sim2a", "8005550103"},
+                    {2, ElementaryFiles.EF_ADN, 2, "Person Sim2b", "8005550104"},
+                    {2, ElementaryFiles.EF_ADN, 3, "Person Sim2c", "8005550105"},
+            });
+            assertThat(cursorSim4).hasData(new Object[][]{
+                    {4, ElementaryFiles.EF_ADN, 1, "Person Sim4", "8005550106"},
+            });
+        }
+    }
+
+    @Test
+    public void query_fdnRecords_returnsFdnData() {
+        setupSimsWithSubscriptionIds(1, 2, 4);
+        mIccPhoneBook.makeAllEfsSupported(1, 2, 4);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Person Sim1", "8005550101");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_ADN, "Person Sim2a", "8005550103");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_FDN, "Person Sim2b", "8005550104");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_FDN, "Person Sim2c", "8005550105");
+        mIccPhoneBook.addRecord(4, IccConstants.EF_SDN, "Person Sim4", "8005550106");
+        mIccPhoneBook.setDefaultSubscriptionId(1);
+
+        String[] projection = {
+                SimRecords.SUBSCRIPTION_ID,
+                SimRecords.ELEMENTARY_FILE_TYPE,
+                SimRecords.RECORD_NUMBER,
+                SimRecords.NAME,
+                SimRecords.PHONE_NUMBER
+        };
+        try (Cursor cursorSim1Fdn = mResolver.query(SimRecords.getContentUri(1, EF_FDN),
+                projection, null, null);
+             Cursor cursorSim2Fdn = mResolver.query(SimRecords.getContentUri(2, EF_FDN),
+                     projection, null, null);
+             Cursor cursorSim4Fdn = mResolver.query(SimRecords.getContentUri(4, EF_FDN),
+                     projection, null, null)
+        ) {
+
+            assertThat(cursorSim1Fdn).hasCount(0);
+            assertThat(cursorSim2Fdn).hasData(new Object[][]{
+                    {2, ElementaryFiles.EF_FDN, 1, "Person Sim2b", "8005550104"},
+                    {2, ElementaryFiles.EF_FDN, 2, "Person Sim2c", "8005550105"},
+            });
+            assertThat(cursorSim4Fdn).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_sdnRecords_returnsSdnData() {
+        setupSimsWithSubscriptionIds(1, 2, 4);
+        mIccPhoneBook.makeAllEfsSupported(1, 2, 4);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Person Adn1", "8005550101");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_FDN, "Person Fdn1", "8005550102");
+        mIccPhoneBook.addRecord(1, IccConstants.EF_SDN, "Person Sdn1", "8005550103");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_ADN, "Person Adn2a", "8005550104");
+        mIccPhoneBook.addRecord(2, IccConstants.EF_FDN, "Person Fdn2b", "8005550105");
+        mIccPhoneBook.addRecord(4, IccConstants.EF_SDN, "Person Sdn4a", "8005550106");
+        mIccPhoneBook.addRecord(4, IccConstants.EF_SDN, "Person Sdn4b", "8005550107");
+        mIccPhoneBook.setDefaultSubscriptionId(1);
+
+        String[] projection = {
+                SimRecords.SUBSCRIPTION_ID,
+                SimRecords.ELEMENTARY_FILE_TYPE,
+                SimRecords.RECORD_NUMBER,
+                SimRecords.NAME,
+                SimRecords.PHONE_NUMBER
+        };
+        try (Cursor cursorSim1Sdn = mResolver.query(SimRecords.getContentUri(1, EF_SDN),
+                projection, null, null);
+             Cursor cursorSim2Sdn = mResolver.query(SimRecords.getContentUri(2, EF_SDN),
+                     projection, null, null);
+             Cursor cursorSim4Sdn = mResolver.query(SimRecords.getContentUri(4, EF_SDN),
+                     projection, null, null)
+        ) {
+
+            assertThat(cursorSim1Sdn)
+                    .atRow(0).hasRowValues(1, ElementaryFiles.EF_SDN, 1, "Person Sdn1",
+                    "8005550103");
+            assertThat(cursorSim2Sdn).hasCount(0);
+            assertThat(cursorSim4Sdn)
+                    .atRow(0).hasRowValues(4, ElementaryFiles.EF_SDN, 1, "Person Sdn4a",
+                    "8005550106")
+                    .atRow(1).hasRowValues(4, ElementaryFiles.EF_SDN, 2, "Person Sdn4b",
+                    "8005550107");
+        }
+    }
+
+    @Test
+    public void query_adnRecords_nonExistentSim_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.query(SimRecords.getContentUri(123, EF_ADN), null, null, null));
+        assertThat(e).hasMessageThat().isEqualTo("No active SIM with subscription ID 123");
+    }
+
+    @Test
+    public void insert_nonExistentSim_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "123");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(123, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo("No active SIM with subscription ID 123");
+    }
+
+    @Test
+    public void update_nonExistentSim_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "123");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.update(SimRecords.getItemUri(123, EF_ADN, 1), values, null));
+        assertThat(e).hasMessageThat().isEqualTo("No active SIM with subscription ID 123");
+    }
+
+    @Test
+    public void delete_nonExistentSim_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.delete(SimRecords.getItemUri(123, EF_ADN, 1), null));
+        assertThat(e).hasMessageThat().isEqualTo("No active SIM with subscription ID 123");
+    }
+
+    @Test
+    public void query_adnRecords_zeroSizeEf_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 0, 0);
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.query(SimRecords.getContentUri(1, EF_ADN), null, null, null));
+        assertThat(e).hasMessageThat().isEqualTo(
+                "adn is not supported for SIM with subscription ID 1");
+    }
+
+    @Test
+    public void query_itemUri_returnsCorrectRow() {
+        setupSimsWithSubscriptionIds(1, 2);
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_ADN, 1, "Name@Adn1[1]", "8005550101"));
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_ADN, 2, "Name@Adn1[2]", "8005550102"));
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_ADN, 3, "Name@Adn1[3]", "8005550103"));
+        mIccPhoneBook.addRecord(2,
+                new AdnRecord(IccConstants.EF_ADN, 3, "Name@Adn2[3]", "8005550104"));
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_FDN, 1, "Name@Fdn1[1]", "8005550105"));
+        mIccPhoneBook.addRecord(2,
+                new AdnRecord(IccConstants.EF_SDN, 1, "Name@Sdn2[1]", "8005550106"));
+
+        String[] projection = {
+                SimRecords.SUBSCRIPTION_ID, SimRecords.ELEMENTARY_FILE_TYPE,
+                SimRecords.RECORD_NUMBER, SimRecords.NAME, SimRecords.PHONE_NUMBER
+        };
+        try (Cursor item1 = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                projection, null, null);
+             Cursor item2 = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 3),
+                     projection, null, null);
+             Cursor item3 = mResolver.query(SimRecords.getItemUri(2, ElementaryFiles.EF_ADN, 3),
+                     projection, null, null);
+             Cursor item4 = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 1),
+                     projection, null, null);
+             Cursor item5 = mResolver.query(SimRecords.getItemUri(2, ElementaryFiles.EF_SDN, 1),
+                     projection, null, null)
+        ) {
+            assertThat(item1).hasSingleRow(1, ElementaryFiles.EF_ADN, 1, "Name@Adn1[1]",
+                    "8005550101");
+            assertThat(item2).hasSingleRow(1, ElementaryFiles.EF_ADN, 3, "Name@Adn1[3]",
+                    "8005550103");
+            assertThat(item3).hasSingleRow(2, ElementaryFiles.EF_ADN, 3, "Name@Adn2[3]",
+                    "8005550104");
+            assertThat(item4).hasSingleRow(1, ElementaryFiles.EF_FDN, 1, "Name@Fdn1[1]",
+                    "8005550105");
+            assertThat(item5).hasSingleRow(2, ElementaryFiles.EF_SDN, 1, "Name@Sdn2[1]",
+                    "8005550106");
+        }
+    }
+
+    @Test
+    public void query_itemUriEmptyRecord_returnsEmptyCursor() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_FDN, 1, 30);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_SDN, 1, 30);
+
+        try (Cursor adnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                null, null, null);
+             Cursor fdnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 1),
+                     null, null, null);
+             Cursor sdnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 1),
+                     null, null, null)
+        ) {
+            assertThat(adnItem).hasCount(0);
+            assertThat(fdnItem).hasCount(0);
+            assertThat(sdnItem).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_itemUriIndexExceedsMax_returnsEmptyCursor() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_FDN, 1, 30);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_SDN, 1, 30);
+
+        try (Cursor adnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2),
+                null, null, null);
+             Cursor fdnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 2),
+                     null, null, null);
+             Cursor sdnItem = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 2),
+                     null, null, null)
+        ) {
+            assertThat(adnItem).hasCount(0);
+            assertThat(fdnItem).hasCount(0);
+            assertThat(sdnItem).hasCount(0);
+        }
+    }
+
+    @Test
+    public void query_invalidItemIndex_throwsIllegalArgumentException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, -1),
+                        null, null, null));
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, -1),
+                        null, null, null));
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, -1),
+                        null, null, null));
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 0),
+                        null, null, null));
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 0),
+                        null, null, null));
+        assertThrows(IllegalArgumentException.class, () ->
+                mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 0),
+                        null, null, null));
+    }
+
+    @Test
+    public void insert_adnRecord_addsAdnRecordAndReturnsUriForNewRecord() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "First Last");
+        values.put(SimRecords.PHONE_NUMBER, "8005550101");
+
+        Uri uri = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        List<AdnRecord> records = mIccPhoneBook.getAdnRecordsInEfForSubscriber(
+                1, IccConstants.EF_ADN).stream()
+                .filter(((Predicate<AdnRecord>) AdnRecord::isEmpty).negate())
+                .collect(Collectors.toList());
+
+        assertThat(records)
+                .comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsExactly(new AdnRecord(IccConstants.EF_ADN, 1, "First Last", "8005550101"));
+
+        assertThat(uri).isEqualTo(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1));
+    }
+
+    @Test
+    public void insert_adnRecordWithExistingRecords_returnsUriWithCorrectIndex() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setDefaultSubscriptionId(1);
+        mIccPhoneBook.addRecord(new AdnRecord(IccConstants.EF_ADN, 2, "Existing1", "8005550101"));
+        mIccPhoneBook.addRecord(new AdnRecord(IccConstants.EF_ADN, 3, "Existing2", "8005550102"));
+        mIccPhoneBook.addRecord(new AdnRecord(IccConstants.EF_ADN, 5, "Existing3", "8005550103"));
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "New1");
+        values.put(SimRecords.PHONE_NUMBER, "8005550104");
+
+        Uri insert1 = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+        values.put(SimRecords.NAME, "New2");
+        values.put(SimRecords.PHONE_NUMBER, "8005550105");
+        Uri insert2 = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+        values.put(SimRecords.NAME, "New3");
+        values.put(SimRecords.PHONE_NUMBER, "8005550106");
+        Uri insert3 = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        assertThat(
+                mIccPhoneBook.getAdnRecordsInEfForSubscriber(1, IccConstants.EF_ADN).subList(0, 6))
+                .comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsExactly(
+                        new AdnRecord(IccConstants.EF_ADN, 1, "New1", "8005550104"),
+                        new AdnRecord(IccConstants.EF_ADN, 2, "Existing1", "8005550101"),
+                        new AdnRecord(IccConstants.EF_ADN, 3, "Existing2", "8005550102"),
+                        new AdnRecord(IccConstants.EF_ADN, 4, "New2", "8005550105"),
+                        new AdnRecord(IccConstants.EF_ADN, 5, "Existing3", "8005550103"),
+                        new AdnRecord(IccConstants.EF_ADN, 6, "New3", "8005550106"));
+        assertThat(insert1).isEqualTo(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1));
+        assertThat(insert2).isEqualTo(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 4));
+        assertThat(insert3).isEqualTo(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 6));
+    }
+
+    @Test
+    public void insert_efFull_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Existing", "8005550101");
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "New");
+        values.put(SimRecords.PHONE_NUMBER, "8005550102");
+
+        Uri uri = SimRecords.getContentUri(1, EF_ADN);
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> mResolver.insert(uri, values));
+        assertThat(e).hasMessageThat().isEqualTo(
+                uri + " is full. Please delete records to add new ones.");
+    }
+
+    @Test
+    public void insert_nullValues_returnsNull() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        Uri result = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), null);
+
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void update_nullValues_returnsZero() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        mIccPhoneBook.addAdnRecord(1, "Name", "5550101");
+
+        int result = mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1), null,
+                null);
+
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void insert_emptyValues_returnsNull() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        Uri result = mResolver.insert(SimRecords.getContentUri(1, EF_ADN), new ContentValues());
+
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void insert_nameOmitted_createsRecordWithJustPhoneNumber() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        // No name
+        values.put(SimRecords.PHONE_NUMBER, "18005550101");
+
+        mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        // Null name
+        values.putNull(SimRecords.NAME);
+        values.put(SimRecords.PHONE_NUMBER, "18005550102");
+        mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        // Empty name
+        values.put(SimRecords.NAME, "");
+        values.put(SimRecords.PHONE_NUMBER, "18005550103");
+        mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        assertThat(mIccPhoneBook.getAllValidRecords())
+                .comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsExactly(
+                        new AdnRecord(IccConstants.EF_ADN, 1, "", "18005550101"),
+                        new AdnRecord(IccConstants.EF_ADN, 2, "", "18005550102"),
+                        new AdnRecord(IccConstants.EF_ADN, 3, "", "18005550103"));
+    }
+
+    @Test
+    public void insert_phoneNumberOmitted_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 25);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo(SimRecords.PHONE_NUMBER + " is required.");
+    }
+
+    @Test
+    public void insert_nameTooLong_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 25);
+
+        ContentValues values = new ContentValues();
+        // Name is limited to 11 characters
+        values.put(SimRecords.NAME, "1234567890ab");
+        values.put(SimRecords.PHONE_NUMBER, "8005550102");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+
+        assertThat(e).hasMessageThat().isEqualTo(SimRecords.NAME + " is too long.");
+    }
+
+    @Test
+    public void insert_phoneNumberTooLong_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 25);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        // 21 digits is longer than max of 20
+        values.put(SimRecords.PHONE_NUMBER, "123456789012345678901");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+
+        assertThat(e).hasMessageThat().isEqualTo(SimRecords.PHONE_NUMBER + " is too long.");
+    }
+
+    @Test
+    public void insert_illegalCharacters_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 32);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "1800J550A0B");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo(
+                SimRecords.PHONE_NUMBER + " contains unsupported characters.");
+
+        values.put(SimRecords.NAME, UNSUPPORTED_NAME);
+        values.put(SimRecords.PHONE_NUMBER, "18005550101");
+
+        e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo(
+                SimRecords.NAME + " contains unsupported characters.");
+
+        values.put(SimRecords.NAME, UNSUPPORTED_NAME2);
+        values.put(SimRecords.PHONE_NUMBER, "18005550101");
+
+        e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo(
+                SimRecords.NAME + " contains unsupported characters.");
+
+        // The inserts didn't actually add any data.
+        assertThat(mIccPhoneBook.getAllValidRecords()).isEmpty();
+    }
+
+    @Test
+    public void insert_unsupportedColumn_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 25);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "18005550101");
+        values.put(SimRecords.RECORD_NUMBER, 8);
+        values.put("extra_phone2", "18005550102");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values));
+        assertThat(e).hasMessageThat().isEqualTo("Unsupported columns: "
+                + SimRecords.RECORD_NUMBER + ",extra_phone2");
+    }
+
+    @Test
+    public void update_existingRecord_updatesRecord() {
+        setupSimsWithSubscriptionIds(1, 2);
+        AdnRecord[] unchanged = new AdnRecord[]{
+                new AdnRecord(IccConstants.EF_ADN, 3, "Other1", "8005550102"),
+                new AdnRecord(IccConstants.EF_ADN, 2, "Other2", "8005550103"),
+                new AdnRecord(IccConstants.EF_FDN, 2, "Other3", "8005550104")
+        };
+        mIccPhoneBook.addRecord(1, unchanged[0]);
+        mIccPhoneBook.addRecord(2, unchanged[1]);
+        mIccPhoneBook.addRecord(2, unchanged[2]);
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_ADN, 2, "Initial Name", "8005550101"));
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Updated Name");
+        values.put(SimRecords.PHONE_NUMBER, "8005550105");
+
+        int result = mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2), values,
+                null);
+
+        assertThat(result).isEqualTo(1);
+
+        List<AdnRecord> finalRecords = mIccPhoneBook.getAllValidRecords();
+
+        assertThat(finalRecords).comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsAtLeastElementsIn(unchanged);
+        assertThat(finalRecords).comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .doesNotContain(
+                        new AdnRecord(IccConstants.EF_ADN, 2, "Initial Name", "80005550101"));
+        assertThat(finalRecords).comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .contains(new AdnRecord(IccConstants.EF_ADN, 2, "Updated Name", "8005550105"));
+    }
+
+    @Test
+    public void update_emptyRecord_updatesRecord() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "name");
+        values.put(SimRecords.PHONE_NUMBER, "18005550101");
+        // No record actually exists with record number 10 but we allow clients to update it
+        // as a way to set the information at a specific record number.
+        int result = mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 10),
+                values, null);
+
+        assertThat(result).isEqualTo(1);
+        List<AdnRecord> finalRecords = mIccPhoneBook.getAllValidRecords();
+        assertThat(finalRecords).comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsExactly(new AdnRecord(IccConstants.EF_ADN, 10, "name", "18005550101"));
+    }
+
+    @Test
+    public void delete_existingRecord_deletesRecord() {
+        setupSimsWithSubscriptionIds(1, 2);
+        AdnRecord[] unchanged = new AdnRecord[]{
+                new AdnRecord(IccConstants.EF_ADN, 3, "Other1", "8005550102"),
+                new AdnRecord(IccConstants.EF_ADN, 2, "Other2", "8005550103"),
+                new AdnRecord(IccConstants.EF_FDN, 2, "Other3", "8005550104")
+        };
+        mIccPhoneBook.addRecord(1,
+                new AdnRecord(IccConstants.EF_ADN, 2, "Initial Name", "8005550101"));
+        mIccPhoneBook.addRecord(1, unchanged[0]);
+        mIccPhoneBook.addRecord(2, unchanged[1]);
+        mIccPhoneBook.addRecord(2, unchanged[2]);
+
+        int result = mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2), null);
+
+        assertThat(result).isEqualTo(1);
+
+        assertThat(mIccPhoneBook.getAllValidRecords()).comparingElementsUsing(ADN_RECORD_IS_EQUAL)
+                .containsExactlyElementsIn(unchanged);
+    }
+
+    @Test
+    public void update_indexExceedingMax_returnsZero() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "name");
+        values.put(SimRecords.PHONE_NUMBER, "18005551010");
+        int result = mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2),
+                values, null);
+
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void update_indexOverflow_throwsIllegalArgumentException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "name");
+        values.put(SimRecords.PHONE_NUMBER, "18005551010");
+        assertThrows(IllegalArgumentException.class, () -> mResolver.update(
+                SimRecords.getContentUri(1, EF_ADN).buildUpon().appendPath(
+                        String.valueOf((Long.MAX_VALUE))).build(),
+                values, null));
+    }
+
+    @Test
+    public void delete_emptyRecord_returnsZero() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        int result = mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2), null);
+
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void delete_indexExceedingMax_returnsZero() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);
+
+        int result = mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 2), null);
+
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void delete_indexOverflow_throwsIllegalArgumentException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        assertThrows(IllegalArgumentException.class, () -> mResolver.delete(
+                SimRecords.getContentUri(1, EF_ADN).buildUpon().appendPath(
+                        String.valueOf((Long.MAX_VALUE))).build(),
+                null));
+    }
+
+    @Test
+    public void update_nameOrNumberTooLong_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 25);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Initial", "8005550101");
+
+        ContentValues values = new ContentValues();
+        // Name is limited to 11 characters
+        values.put(SimRecords.NAME, "1234567890ab");
+        values.put(SimRecords.PHONE_NUMBER, "8005550102");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.update(SimRecords.getItemUri(
+                        1, ElementaryFiles.EF_ADN, 1), values, null));
+        assertThat(e).hasMessageThat().isEqualTo(SimRecords.NAME + " is too long.");
+
+        values.put(SimRecords.NAME, "abc");
+        values.put(SimRecords.PHONE_NUMBER, "123456789012345678901");
+
+        e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                        values,
+                        null));
+        assertThat(e).hasMessageThat().isEqualTo(SimRecords.PHONE_NUMBER + " is too long.");
+        // The updates didn't actually change the data
+        assertThat(mIccPhoneBook.getAllValidRecords())
+                .comparingElementsUsing(Correspondence.from(AdnRecord::isEqual, "isEqual"))
+                .containsExactly(new AdnRecord(IccConstants.EF_ADN, 1, "Initial", "8005550101"));
+    }
+
+    @Test
+    public void update_nameOrNumberWithInvalidCharacters_throwsCorrectException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 32);
+        mIccPhoneBook.addRecord(1, IccConstants.EF_ADN, "Initial", "8005550101");
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "(800)555-0190 x7777");
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                        values,
+                        null));
+        assertThat(e).hasMessageThat().isEqualTo(
+                SimRecords.PHONE_NUMBER + " contains unsupported characters.");
+
+        // Unicode fffe is a unicode non-character
+        values.put(SimRecords.NAME, UNSUPPORTED_NAME);
+        values.put(SimRecords.PHONE_NUMBER, "18005550102");
+
+        e = assertThrows(IllegalArgumentException.class,
+                () -> mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                        values,
+                        null));
+        assertThat(e).hasMessageThat().isEqualTo(
+                SimRecords.NAME + " contains unsupported characters.");
+
+        // The updates didn't actually change the data.
+        assertThat(mIccPhoneBook.getAllValidRecords())
+                .comparingElementsUsing(Correspondence.from(AdnRecord::isEqual, "isEqual"))
+                .containsExactly(new AdnRecord(IccConstants.EF_ADN, 1, "Initial", "8005550101"));
+    }
+
+    @Test
+    public void insert_nonAdnDirUris_throwsUnsupportedOperationException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "8005550101");
+
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.insert(ElementaryFiles.CONTENT_URI, values));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_FDN), values));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.insert(SimRecords.getContentUri(1, EF_SDN), values));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.insert(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 1), values));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.insert(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 1), values));
+    }
+
+    @Test
+    public void update_nonAdnDirUris_throwsUnsupportedOperationException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "8005550101");
+
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.update(ElementaryFiles.CONTENT_URI, values, null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.update(SimRecords.getContentUri(1, EF_FDN), values, null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.update(SimRecords.getContentUri(1, EF_SDN), values, null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.update(SimRecords.getContentUri(1, EF_SDN), values, null));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 1), values,
+                        null));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 1), values,
+                        null));
+    }
+
+    @Test
+    public void delete_nonAdnDirUris_throwsUnsupportedOperationException() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Name");
+        values.put(SimRecords.PHONE_NUMBER, "8005550101");
+
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.delete(ElementaryFiles.CONTENT_URI, null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.delete(SimRecords.getContentUri(1, EF_FDN), null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.delete(SimRecords.getContentUri(1, EF_SDN), null));
+        assertThrows(UnsupportedOperationException.class,
+                () -> mResolver.delete(SimRecords.getContentUri(1, EF_SDN), null));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_FDN, 1), null));
+        assertThrows(UnsupportedOperationException.class, () ->
+                mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_SDN, 1), null));
+    }
+
+    @Test
+    public void subscriptionsChange_callsNotifyChange() {
+        // Clear invocations that happened in setUp
+        Mockito.reset(mMockSubscriptionManager);
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        SimPhonebookProvider.ContentNotifier mockNotifier = mock(
+                SimPhonebookProvider.ContentNotifier.class);
+        ArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener> listenerCaptor =
+                ArgumentCaptor.forClass(SubscriptionManager.OnSubscriptionsChangedListener.class);
+
+        TestableSimPhonebookProvider.setup(
+                mResolver, mMockSubscriptionManager, mIccPhoneBook, mockNotifier);
+        verify(mMockSubscriptionManager).addOnSubscriptionsChangedListener(
+                any(), listenerCaptor.capture());
+        listenerCaptor.getValue().onSubscriptionsChanged();
+        setupSimsWithSubscriptionIds(1, 2);
+        listenerCaptor.getValue().onSubscriptionsChanged();
+        listenerCaptor.getValue().onSubscriptionsChanged();
+
+        verify(mockNotifier, times(2)).notifyChange(eq(SimPhonebookContract.AUTHORITY_URI));
+    }
+
+    @Test
+    public void insert_callsNotifyChange() {
+        // Clear invocations that happened in setUp
+        Mockito.reset(mMockSubscriptionManager);
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+        SimPhonebookProvider.ContentNotifier mockNotifier = mock(
+                SimPhonebookProvider.ContentNotifier.class);
+
+        TestableSimPhonebookProvider.setup(
+                mResolver, mMockSubscriptionManager, mIccPhoneBook, mockNotifier);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "name");
+        values.put(SimRecords.PHONE_NUMBER, "5550101");
+        mResolver.insert(SimRecords.getContentUri(1, EF_ADN), values);
+
+        verify(mockNotifier).notifyChange(eq(SimPhonebookContract.AUTHORITY_URI));
+    }
+
+    @Test
+    public void update_callsNotifyChange() {
+        // Clear invocations that happened in setUp
+        Mockito.reset(mMockSubscriptionManager);
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.addAdnRecord(1, "Initial", "5550101");
+        SimPhonebookProvider.ContentNotifier mockNotifier = mock(
+                SimPhonebookProvider.ContentNotifier.class);
+
+        TestableSimPhonebookProvider.setup(
+                mResolver, mMockSubscriptionManager, mIccPhoneBook, mockNotifier);
+
+        ContentValues values = new ContentValues();
+        values.put(SimRecords.NAME, "Updated");
+        values.put(SimRecords.PHONE_NUMBER, "5550102");
+        mResolver.update(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1), values, null);
+
+        verify(mockNotifier).notifyChange(eq(SimPhonebookContract.AUTHORITY_URI));
+    }
+
+    @Test
+    public void delete_callsNotifyChange() {
+        // Clear invocations that happened in setUp
+        Mockito.reset(mMockSubscriptionManager);
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.addAdnRecord(1, "Initial", "5550101");
+        SimPhonebookProvider.ContentNotifier mockNotifier = mock(
+                SimPhonebookProvider.ContentNotifier.class);
+
+        TestableSimPhonebookProvider.setup(
+                mResolver, mMockSubscriptionManager, mIccPhoneBook, mockNotifier);
+
+        mResolver.delete(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1), null);
+
+        verify(mockNotifier).notifyChange(eq(SimPhonebookContract.AUTHORITY_URI));
+    }
+
+    @Test
+    public void validateName_validName_returnsValueIsCorrect() {
+        setupSimsWithSubscriptionIds(1);
+        String validName = "First Last";
+        // See AdnRecord#FOOTER_SIZE_BYTES
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 10, validName.length() + 14);
+        SimRecords.NameValidationResult validationResult = SimRecords.validateName(mResolver, 1,
+                EF_ADN, validName);
+
+        assertThat(validationResult.isValid()).isTrue();
+        assertThat(validationResult.getName()).isEqualTo(validName);
+        assertThat(validationResult.getSanitizedName()).isEqualTo(validName);
+        assertThat(validationResult.getEncodedLength()).isEqualTo(validName.length());
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(validName.length());
+
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 10, 40);
+        validationResult = SimRecords.validateName(mResolver, 1, EF_ADN, validName);
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(40 - 14);
+    }
+
+    @Test
+    public void validateName_nameTooLong_returnsValueIsCorrect() {
+        setupSimsWithSubscriptionIds(1);
+        String tooLongName = "First Last";
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 10, tooLongName.length() + 14 - 1);
+        SimRecords.NameValidationResult validationResult = SimRecords.validateName(mResolver, 1,
+                EF_ADN, tooLongName);
+
+        assertThat(validationResult.isValid()).isFalse();
+        assertThat(validationResult.getName()).isEqualTo(tooLongName);
+        assertThat(validationResult.getSanitizedName()).isEqualTo(tooLongName);
+        assertThat(validationResult.getEncodedLength()).isEqualTo(tooLongName.length());
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(tooLongName.length() - 1);
+    }
+
+    @Test
+    public void validateName_nameWithUnsupportedCharacters_returnsValueIsCorrect() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 10, 40);
+        SimRecords.NameValidationResult validationResult = SimRecords.validateName(mResolver, 1,
+                EF_ADN, UNSUPPORTED_NAME);
+
+        assertThat(validationResult.isValid()).isFalse();
+        assertThat(validationResult.getName()).isEqualTo(UNSUPPORTED_NAME);
+        assertThat(validationResult.getSanitizedName()).isEqualTo(":)=  ;ni= ;hon= ;");
+        assertThat(validationResult.getEncodedLength()).isEqualTo(UNSUPPORTED_NAME.length());
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(
+                AdnRecord.getMaxAlphaTagBytes(40));
+    }
+
+    @Test
+    public void validateName_emptyString_returnsValueIsCorrect() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 10, 40);
+        SimRecords.NameValidationResult validationResult = SimRecords.validateName(mResolver, 1,
+                EF_ADN, "");
+
+        assertThat(validationResult.isValid()).isTrue();
+        assertThat(validationResult.getName()).isEqualTo("");
+        assertThat(validationResult.getSanitizedName()).isEqualTo("");
+        assertThat(validationResult.getEncodedLength()).isEqualTo(0);
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(
+                AdnRecord.getMaxAlphaTagBytes(40));
+
+        // Null is equivalent to empty
+        validationResult = SimRecords.validateName(mResolver, 1, EF_ADN, null);
+        assertThat(validationResult.getName()).isEqualTo("");
+        assertThat(validationResult.getSanitizedName()).isEqualTo("");
+        assertThat(validationResult.getEncodedLength()).isEqualTo(0);
+        assertThat(validationResult.getMaxEncodedLength()).isEqualTo(
+                AdnRecord.getMaxAlphaTagBytes(40));
+    }
+
+    private void setupSimsWithSubscriptionIds(int... subscriptionIds) {
+        when(mMockSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(subscriptionIds);
+        when(mMockSubscriptionManager.getActiveSubscriptionInfoCount())
+                .thenReturn(subscriptionIds.length);
+        List<SubscriptionInfo> subscriptions = createSubscriptionsWithIds(subscriptionIds);
+        when(mMockSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subscriptions);
+        for (SubscriptionInfo info : subscriptions) {
+            when(mMockSubscriptionManager.getActiveSubscriptionInfo(info.getSubscriptionId()))
+                    .thenReturn(info);
+        }
+    }
+
+    public static class FakeIccPhoneBook extends IIccPhoneBook.Default {
+
+        private static final int DEFAULT_RECORD_SIZE = 30;
+        private static final int DEFAULT_RECORDS_COUNT = 100;
+
+        // The key for both maps is the (subscription ID, efid)
+        private Map<Pair<Integer, Integer>, AdnRecord[]> mRecords = new HashMap<>();
+        // The value is the single record size
+        private Map<Pair<Integer, Integer>, Integer> mRecordSizes = new HashMap<>();
+
+        private int mDefaultSubscriptionId = 101;
+
+        private void addRecord(Pair<Integer, Integer> key, AdnRecord record) {
+            // Assume that if records are being added then the test wants it to be a valid
+            // elementary file so set sizes as well.
+            if (!mRecordSizes.containsKey(key)) {
+                setRecordsSize(key.first, key.second,
+                        Math.max(record.getRecId(), DEFAULT_RECORDS_COUNT), DEFAULT_RECORD_SIZE);
+            }
+            mRecords.get(key)[record.getRecId() - 1] = record;
+        }
+
+        public void addRecord(AdnRecord record) {
+            addRecord(Pair.create(mDefaultSubscriptionId, record.getEfid()), record);
+        }
+
+        public void addRecord(int subscriptionId, AdnRecord record) {
+            addRecord(Pair.create(subscriptionId, record.getEfid()), record);
+        }
+
+        public void addRecord(int subscriptionId, int efId, String name, String phoneNumber) {
+            Pair<Integer, Integer> key = Pair.create(subscriptionId, efId);
+            AdnRecord[] records = mRecords.computeIfAbsent(key, unused ->
+                    createEmptyRecords(efId, 100));
+            int recordIndex = -1;
+            for (int i = 0; i < records.length; i++) {
+                if (records[i].isEmpty()) {
+                    recordIndex = i;
+                    break;
+                }
+            }
+            if (recordIndex == -1) {
+                throw new IllegalStateException("");
+            }
+            addRecord(key, new AdnRecord(efId, recordIndex + 1, name, phoneNumber));
+        }
+
+        public void addAdnRecord(int subscriptionId, String name, String phoneNumber) {
+            addRecord(subscriptionId, IccConstants.EF_ADN, name, phoneNumber);
+        }
+
+        public void addAdnRecord(String name, String phoneNumber) {
+            addRecord(mDefaultSubscriptionId, IccConstants.EF_ADN, name, phoneNumber);
+        }
+
+        public List<AdnRecord> getAllValidRecords() {
+            List<AdnRecord> result = new ArrayList<>();
+            for (AdnRecord[] records : mRecords.values()) {
+                for (AdnRecord record : records) {
+                    if (!record.isEmpty()) {
+                        result.add(record);
+                    }
+                }
+            }
+            return result;
+        }
+
+        public void makeAllEfsSupported() {
+            makeAllEfsSupported(mDefaultSubscriptionId);
+        }
+
+        /**
+         * Sets up the fake to return valid records size for all elementary files for the provided
+         * subscription IDs.
+         */
+        public void makeAllEfsSupported(int... subscriptionIds) {
+            for (int subId : subscriptionIds) {
+                makeAllEfsSupported(subId);
+            }
+        }
+
+        /**
+         * Sets up the fake to return valid records size for all elementary files for the provided
+         * subscription IDs.
+         */
+        public void makeAllEfsSupported(int subscriptionId) {
+            setRecordsSize(subscriptionId, IccConstants.EF_ADN, DEFAULT_RECORDS_COUNT,
+                    DEFAULT_RECORD_SIZE);
+            setRecordsSize(subscriptionId, IccConstants.EF_FDN, DEFAULT_RECORDS_COUNT,
+                    DEFAULT_RECORD_SIZE);
+            setRecordsSize(subscriptionId, IccConstants.EF_SDN, DEFAULT_RECORDS_COUNT,
+                    DEFAULT_RECORD_SIZE);
+        }
+
+        public void setRecordsSize(int subscriptionId, int efid, int maxRecordCount,
+                int maxRecordSize) {
+            Pair<Integer, Integer> key = Pair.create(subscriptionId, efid);
+            mRecordSizes.put(key, maxRecordSize);
+            AdnRecord[] records = mRecords.computeIfAbsent(key, unused ->
+                    createEmptyRecords(efid, maxRecordCount));
+            if (records.length < maxRecordCount) {
+                throw new IllegalStateException("Records already initialized with a smaller size");
+            }
+        }
+
+        private AdnRecord[] createEmptyRecords(int efid, int count) {
+            AdnRecord[] records = new AdnRecord[count];
+            for (int i = 0; i < records.length; i++) {
+                if (records[i] == null) {
+                    records[i] = new AdnRecord(efid, i + 1, "", "");
+                }
+            }
+            return records;
+        }
+
+        public void setDefaultSubscriptionId(int defaultSubscriptionId) {
+            mDefaultSubscriptionId = defaultSubscriptionId;
+        }
+
+        public void reset() {
+            mRecords.clear();
+            mRecordSizes.clear();
+        }
+
+        @Override
+        public List<AdnRecord> getAdnRecordsInEf(int efid) {
+            return getAdnRecordsInEfForSubscriber(mDefaultSubscriptionId, efid);
+        }
+
+        @Override
+        public List<AdnRecord> getAdnRecordsInEfForSubscriber(int subId, int efid) {
+            return Arrays.asList(
+                    mRecords.getOrDefault(Pair.create(subId, efid), new AdnRecord[0]));
+        }
+
+        @Override
+        public boolean updateAdnRecordsInEfBySearch(int efid, String oldTag, String oldPhoneNumber,
+                String newTag, String newPhoneNumber, String pin2) {
+            return updateAdnRecordsInEfBySearchForSubscriber(
+                    mDefaultSubscriptionId, efid,
+                    oldTag, oldPhoneNumber, newTag, newPhoneNumber, pin2);
+        }
+
+        @Override
+        public boolean updateAdnRecordsInEfBySearchForSubscriber(int subId, int efid, String oldTag,
+                String oldPhoneNumber, String newTag, String newPhoneNumber, String pin2) {
+            if (!oldTag.isEmpty() || !oldPhoneNumber.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "updateAdnRecordsInEfBySearchForSubscriber only supports insert");
+            }
+            addRecord(subId, efid, newTag, newPhoneNumber);
+            return true;
+        }
+
+        @Override
+        public boolean updateAdnRecordsInEfByIndex(int efid, String newTag, String newPhoneNumber,
+                int index, String pin2) {
+            return updateAdnRecordsInEfByIndexForSubscriber(mDefaultSubscriptionId,
+                    efid, newTag, newPhoneNumber, index, pin2);
+        }
+
+        @Override
+        public boolean updateAdnRecordsInEfByIndexForSubscriber(int subId, int efid, String newTag,
+                String newPhoneNumber, int index, String pin2) {
+            AdnRecord[] records = mRecords.computeIfAbsent(Pair.create(subId, efid), unused ->
+                    createEmptyRecords(efid, 100));
+            records[index - 1] = new AdnRecord(efid, index, newTag, newPhoneNumber);
+            return true;
+        }
+
+        @Override
+        public int[] getAdnRecordsSize(int efid) {
+            return getAdnRecordsSizeForSubscriber(mDefaultSubscriptionId, efid);
+        }
+
+        @Override
+        public int[] getAdnRecordsSizeForSubscriber(int subId, int efid) {
+            Pair<Integer, Integer> key = Pair.create(subId, efid);
+            Integer recordSize = mRecordSizes.get(key);
+            if (recordSize == null) {
+                return new int[]{0, 0, 0};
+            }
+            int count = mRecords.get(key).length;
+            return new int[]{recordSize, recordSize * count, count};
+        }
+    }
+
+    /**
+     * Implementation of SimPhonebookProvider that allows test-doubles to be injected.
+     *
+     * <p>The ProviderTestRule doesn't seem to allow a better way to do this since it just
+     * invokes the constructor.
+     */
+    public static class TestableSimPhonebookProvider extends SimPhonebookProvider {
+
+        public static void setup(
+                ContentResolver resolver,
+                SubscriptionManager subscriptionManager,
+                IIccPhoneBook iccPhoneBook) {
+            setup(resolver, subscriptionManager, iccPhoneBook, uri -> {
+            });
+        }
+
+        public static void setup(
+                ContentResolver resolver,
+                SubscriptionManager subscriptionManager,
+                IIccPhoneBook iccPhoneBook,
+                ContentNotifier notifier) {
+            TestableSimPhonebookProvider provider =
+                    (TestableSimPhonebookProvider) Objects.requireNonNull(
+                            resolver.acquireContentProviderClient(
+                                    SimPhonebookContract.AUTHORITY))
+                            .getLocalContentProvider();
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
+                    provider.onCreate(subscriptionManager, () -> iccPhoneBook, notifier));
+        }
+
+        @Override
+        public boolean onCreate() {
+            // We stub super.onCreate because it initializes services which causes an
+            // IllegalArgumentException because of the context used for the test.
+            return true;
+        }
+    }
+}