Adding SimPhonebookProvider

This is the provider that implements SimPhonebookContract.

Test: atest TeleServiceTests:SimPhonebookProviderTest
Bug: 154363919
Change-Id: I584150dd8352dbb49d70ea28c6d064a6336d018d
diff --git a/src/com/android/phone/SimPhonebookProvider.java b/src/com/android/phone/SimPhonebookProvider.java
new file mode 100644
index 0000000..8307672
--- /dev/null
+++ b/src/com/android/phone/SimPhonebookProvider.java
@@ -0,0 +1,932 @@
+/*
+ * 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 android.Manifest;
+import android.annotation.TestApi;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.RemoteException;
+import android.provider.SimPhonebookContract;
+import android.provider.SimPhonebookContract.ElementaryFiles;
+import android.provider.SimPhonebookContract.SimRecords;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyFrameworkInitializer;
+import android.telephony.TelephonyManager;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IIccPhoneBook;
+import com.android.internal.telephony.uicc.AdnRecord;
+import com.android.internal.telephony.uicc.IccConstants;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
+
+/**
+ * Provider for contact records stored on the SIM card.
+ *
+ * @see SimPhonebookContract
+ */
+public class SimPhonebookProvider extends ContentProvider {
+
+    @VisibleForTesting
+    static final String[] ELEMENTARY_FILES_ALL_COLUMNS = {
+            ElementaryFiles.SLOT_INDEX,
+            ElementaryFiles.SUBSCRIPTION_ID,
+            ElementaryFiles.EF_TYPE,
+            ElementaryFiles.MAX_RECORDS,
+            ElementaryFiles.RECORD_COUNT,
+            ElementaryFiles.NAME_MAX_LENGTH,
+            ElementaryFiles.PHONE_NUMBER_MAX_LENGTH
+    };
+    @VisibleForTesting
+    static final String[] SIM_RECORDS_ALL_COLUMNS = {
+            SimRecords.SUBSCRIPTION_ID,
+            SimRecords.ELEMENTARY_FILE_TYPE,
+            SimRecords.RECORD_NUMBER,
+            SimRecords.NAME,
+            SimRecords.PHONE_NUMBER
+    };
+    private static final String TAG = "SimPhonebookProvider";
+    private static final Set<String> ELEMENTARY_FILES_COLUMNS_SET =
+            ImmutableSet.copyOf(ELEMENTARY_FILES_ALL_COLUMNS);
+    private static final Set<String> SIM_RECORDS_WRITABLE_COLUMNS = ImmutableSet.of(
+            SimRecords.NAME, SimRecords.PHONE_NUMBER
+    );
+
+    private static final int WRITE_TIMEOUT_SECONDS = 30;
+
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final int ELEMENTARY_FILES = 100;
+    private static final int ELEMENTARY_FILES_ITEM = 101;
+    private static final int SIM_RECORDS = 200;
+    private static final int SIM_RECORDS_ITEM = 201;
+    private static final int VALIDATE_NAME = 300;
+
+    static {
+        URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
+                ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES);
+        URI_MATCHER.addURI(
+                SimPhonebookContract.AUTHORITY,
+                ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/"
+                        + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*",
+                ELEMENTARY_FILES_ITEM);
+        URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
+                SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS);
+        URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
+                SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM);
+        URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
+                SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/"
+                        + SimRecords.VALIDATE_NAME_PATH_SEGMENT,
+                VALIDATE_NAME);
+    }
+
+    // Only allow 1 write at a time to prevent races; the mutations are based on reads of the
+    // existing list of records which means concurrent writes would be problematic.
+    private final Lock mWriteLock = new ReentrantLock(true);
+    private SubscriptionManager mSubscriptionManager;
+    private Supplier<IIccPhoneBook> mIccPhoneBookSupplier;
+    private ContentNotifier mContentNotifier;
+
+    static int efIdForEfType(@ElementaryFiles.EfType int efType) {
+        switch (efType) {
+            case ElementaryFiles.EF_ADN:
+                return IccConstants.EF_ADN;
+            case ElementaryFiles.EF_FDN:
+                return IccConstants.EF_FDN;
+            case ElementaryFiles.EF_SDN:
+                return IccConstants.EF_SDN;
+            default:
+                return 0;
+        }
+    }
+
+    private static void validateProjection(Set<String> allowed, String[] projection) {
+        if (projection == null || allowed.containsAll(Arrays.asList(projection))) {
+            return;
+        }
+        Set<String> invalidColumns = new LinkedHashSet<>(Arrays.asList(projection));
+        invalidColumns.removeAll(allowed);
+        throw new IllegalArgumentException(
+                "Unsupported columns: " + Joiner.on(",").join(invalidColumns));
+    }
+
+    private static int getRecordSize(int[] recordsSize) {
+        return recordsSize[0];
+    }
+
+    private static int getRecordCount(int[] recordsSize) {
+        return recordsSize[2];
+    }
+
+    /** Returns the IccPhoneBook used to load the AdnRecords. */
+    private static IIccPhoneBook getIccPhoneBook() {
+        return IIccPhoneBook.Stub.asInterface(TelephonyFrameworkInitializer
+                .getTelephonyServiceManager().getIccPhoneBookServiceRegisterer().get());
+    }
+
+    @Override
+    public boolean onCreate() {
+        ContentResolver resolver = getContext().getContentResolver();
+        return onCreate(getContext().getSystemService(SubscriptionManager.class),
+                SimPhonebookProvider::getIccPhoneBook,
+                uri -> resolver.notifyChange(uri, null));
+    }
+
+    @TestApi
+    boolean onCreate(SubscriptionManager subscriptionManager,
+            Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier) {
+        if (subscriptionManager == null) {
+            return false;
+        }
+        mSubscriptionManager = subscriptionManager;
+        mIccPhoneBookSupplier = iccPhoneBookSupplier;
+        mContentNotifier = notifier;
+
+        mSubscriptionManager.addOnSubscriptionsChangedListener(MoreExecutors.directExecutor(),
+                new SubscriptionManager.OnSubscriptionsChangedListener() {
+                    boolean mFirstCallback = true;
+                    private int[] mNotifiedSubIds = {};
+
+                    @Override
+                    public void onSubscriptionsChanged() {
+                        if (mFirstCallback) {
+                            mFirstCallback = false;
+                            return;
+                        }
+                        int[] activeSubIds = mSubscriptionManager.getActiveSubscriptionIdList();
+                        if (!Arrays.equals(mNotifiedSubIds, activeSubIds)) {
+                            notifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
+                            mNotifiedSubIds = Arrays.copyOf(activeSubIds, activeSubIds.length);
+                        }
+                    }
+                });
+        return true;
+    }
+
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs,
+            @Nullable CancellationSignal cancellationSignal) {
+        if (queryArgs != null && (queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION)
+                || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS)
+                || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_LIMIT))) {
+            throw new IllegalArgumentException(
+                    "A SQL selection was provided but it is not supported by this provider.");
+        }
+        switch (URI_MATCHER.match(uri)) {
+            case ELEMENTARY_FILES:
+                return queryElementaryFiles(projection);
+            case ELEMENTARY_FILES_ITEM:
+                return queryElementaryFilesItem(PhonebookArgs.forElementaryFilesItem(uri),
+                        projection);
+            case SIM_RECORDS:
+                return querySimRecords(PhonebookArgs.forSimRecords(uri, queryArgs), projection);
+            case SIM_RECORDS_ITEM:
+                return querySimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, queryArgs),
+                        projection);
+            case VALIDATE_NAME:
+                return queryValidateName(PhonebookArgs.forValidateName(uri, queryArgs), queryArgs);
+            default:
+                throw new IllegalArgumentException("Unsupported Uri " + uri);
+        }
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
+            @Nullable String[] selectionArgs, @Nullable String sortOrder,
+            @Nullable CancellationSignal cancellationSignal) {
+        throw new UnsupportedOperationException("Only query with Bundle is supported");
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
+            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+        throw new UnsupportedOperationException("Only query with Bundle is supported");
+    }
+
+    private Cursor queryElementaryFiles(String[] projection) {
+        validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
+        if (projection == null) {
+            projection = ELEMENTARY_FILES_ALL_COLUMNS;
+        }
+
+        MatrixCursor result = new MatrixCursor(projection);
+
+        List<SubscriptionInfo> activeSubscriptions = getActiveSubscriptionInfoList();
+        for (SubscriptionInfo subInfo : activeSubscriptions) {
+            try {
+                addEfToCursor(result, subInfo, ElementaryFiles.EF_ADN);
+                addEfToCursor(result, subInfo, ElementaryFiles.EF_FDN);
+                addEfToCursor(result, subInfo, ElementaryFiles.EF_SDN);
+            } catch (RemoteException e) {
+                // Return an empty cursor. If service to access it is throwing remote
+                // exceptions then it's basically the same as not having a SIM.
+                return new MatrixCursor(projection, 0);
+            }
+        }
+        return result;
+    }
+
+    private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) {
+        validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
+
+        MatrixCursor result = new MatrixCursor(projection);
+        try {
+            addEfToCursor(
+                    result, getActiveSubscriptionInfo(args.subscriptionId), args.efType);
+        } catch (RemoteException e) {
+            // Return an empty cursor. If service to access it is throwing remote
+            // exceptions then it's basically the same as not having a SIM.
+            return new MatrixCursor(projection, 0);
+        }
+        return result;
+    }
+
+    private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
+            int efType) throws RemoteException {
+        int[] recordsSize = mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
+                subscriptionInfo.getSubscriptionId(), efIdForEfType(efType));
+        addEfToCursor(result, subscriptionInfo, efType, recordsSize);
+    }
+
+    private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
+            int efType, int[] recordsSize) throws RemoteException {
+        // If the record count is zero then the SIM doesn't support the elementary file so just
+        // omit it.
+        if (recordsSize == null || getRecordCount(recordsSize) == 0) {
+            return;
+        }
+        MatrixCursor.RowBuilder row = result.newRow()
+                .add(ElementaryFiles.SLOT_INDEX, subscriptionInfo.getSimSlotIndex())
+                .add(ElementaryFiles.SUBSCRIPTION_ID, subscriptionInfo.getSubscriptionId())
+                .add(ElementaryFiles.EF_TYPE, efType)
+                .add(ElementaryFiles.MAX_RECORDS, getRecordCount(recordsSize))
+                .add(ElementaryFiles.NAME_MAX_LENGTH,
+                        AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)))
+                .add(ElementaryFiles.PHONE_NUMBER_MAX_LENGTH,
+                        AdnRecord.getMaxPhoneNumberDigits());
+        if (result.getColumnIndex(ElementaryFiles.RECORD_COUNT) != -1) {
+            int efid = efIdForEfType(efType);
+            List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get()
+                    .getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid);
+            int nonEmptyCount = 0;
+            for (AdnRecord record : existingRecords) {
+                if (!record.isEmpty()) {
+                    nonEmptyCount++;
+                }
+            }
+            row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount);
+        }
+    }
+
+    private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
+        validateSubscriptionAndEf(args);
+        if (projection == null) {
+            projection = SIM_RECORDS_ALL_COLUMNS;
+        }
+
+        List<AdnRecord> records = loadRecordsForEf(args);
+        if (records == null) {
+            return new MatrixCursor(projection, 0);
+        }
+        MatrixCursor result = new MatrixCursor(projection, records.size());
+        List<Pair<AdnRecord, MatrixCursor.RowBuilder>> rowBuilders = new ArrayList<>(
+                records.size());
+        for (AdnRecord record : records) {
+            if (!record.isEmpty()) {
+                rowBuilders.add(Pair.create(record, result.newRow()));
+            }
+        }
+        // This is kind of ugly but avoids looking up columns in an inner loop.
+        for (String column : projection) {
+            switch (column) {
+                case SimRecords.SUBSCRIPTION_ID:
+                    for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
+                        row.second.add(args.subscriptionId);
+                    }
+                    break;
+                case SimRecords.ELEMENTARY_FILE_TYPE:
+                    for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
+                        row.second.add(args.efType);
+                    }
+                    break;
+                case SimRecords.RECORD_NUMBER:
+                    for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
+                        row.second.add(row.first.getRecId());
+                    }
+                    break;
+                case SimRecords.NAME:
+                    for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
+                        row.second.add(row.first.getAlphaTag());
+                    }
+                    break;
+                case SimRecords.PHONE_NUMBER:
+                    for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
+                        row.second.add(row.first.getNumber());
+                    }
+                    break;
+                default:
+                    Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
+                    break;
+            }
+        }
+        return result;
+    }
+
+    private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
+        if (projection == null) {
+            projection = SIM_RECORDS_ALL_COLUMNS;
+        }
+        validateSubscriptionAndEf(args);
+        AdnRecord record = loadRecord(args);
+
+        MatrixCursor result = new MatrixCursor(projection, 1);
+        if (record == null || record.isEmpty()) {
+            return result;
+        }
+        result.newRow()
+                .add(SimRecords.SUBSCRIPTION_ID, args.subscriptionId)
+                .add(SimRecords.ELEMENTARY_FILE_TYPE, args.efType)
+                .add(SimRecords.RECORD_NUMBER, record.getRecId())
+                .add(SimRecords.NAME, record.getAlphaTag())
+                .add(SimRecords.PHONE_NUMBER, record.getNumber());
+        return result;
+    }
+
+    private Cursor queryValidateName(PhonebookArgs args, @Nullable Bundle queryArgs) {
+        if (queryArgs == null) {
+            throw new IllegalArgumentException(SimRecords.NAME + " is required.");
+        }
+        validateSubscriptionAndEf(args);
+        String name = queryArgs.getString(SimRecords.NAME);
+
+        // Cursor extras are used to return the result.
+        Cursor result = new MatrixCursor(new String[0], 0);
+        Bundle extras = new Bundle();
+        extras.putParcelable(SimRecords.EXTRA_NAME_VALIDATION_RESULT, validateName(args, name));
+        result.setExtras(extras);
+        return result;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        switch (URI_MATCHER.match(uri)) {
+            case ELEMENTARY_FILES:
+                return ElementaryFiles.CONTENT_TYPE;
+            case ELEMENTARY_FILES_ITEM:
+                return ElementaryFiles.CONTENT_ITEM_TYPE;
+            case SIM_RECORDS:
+                return SimRecords.CONTENT_TYPE;
+            case SIM_RECORDS_ITEM:
+                return SimRecords.CONTENT_ITEM_TYPE;
+            default:
+                throw new IllegalArgumentException("Unsupported Uri " + uri);
+        }
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return insert(uri, values, null);
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
+        switch (URI_MATCHER.match(uri)) {
+            case SIM_RECORDS:
+                return insertSimRecord(PhonebookArgs.forSimRecords(uri, extras), values);
+            case ELEMENTARY_FILES:
+            case ELEMENTARY_FILES_ITEM:
+            case SIM_RECORDS_ITEM:
+                throw new UnsupportedOperationException(uri + " does not support insert");
+            default:
+                throw new IllegalArgumentException("Unsupported Uri " + uri);
+        }
+    }
+
+    private Uri insertSimRecord(PhonebookArgs args, ContentValues values) {
+        validateWritableEf(args, "insert");
+        validateSubscriptionAndEf(args);
+
+        if (values == null || values.isEmpty()) {
+            return null;
+        }
+        validateValues(args, values);
+        String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
+        String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
+
+        acquireWriteLockOrThrow();
+        try {
+            List<AdnRecord> records = loadRecordsForEf(args);
+            if (records == null) {
+                Rlog.e(TAG, "Failed to load existing records for " + args.uri);
+                return null;
+            }
+            AdnRecord emptyRecord = null;
+            for (AdnRecord record : records) {
+                if (record.isEmpty()) {
+                    emptyRecord = record;
+                    break;
+                }
+            }
+            if (emptyRecord == null) {
+                // When there are no empty records that means the EF is full.
+                throw new IllegalStateException(
+                        args.uri + " is full. Please delete records to add new ones.");
+            }
+            boolean success = updateRecord(args, emptyRecord, args.pin2, newName, newPhoneNumber);
+            if (!success) {
+                Rlog.e(TAG, "Insert failed for " + args.uri);
+                // Something didn't work but since we don't have any more specific
+                // information to provide to the caller it's better to just return null
+                // rather than throwing and possibly crashing their process.
+                return null;
+            }
+            notifyChange();
+            return SimRecords.getItemUri(args.subscriptionId, args.efType, emptyRecord.getRecId());
+        } finally {
+            releaseWriteLock();
+        }
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        throw new UnsupportedOperationException("Only delete with Bundle is supported");
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
+        switch (URI_MATCHER.match(uri)) {
+            case SIM_RECORDS_ITEM:
+                return deleteSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras));
+            case ELEMENTARY_FILES:
+            case ELEMENTARY_FILES_ITEM:
+            case SIM_RECORDS:
+                throw new UnsupportedOperationException(uri + " does not support delete");
+            default:
+                throw new IllegalArgumentException("Unsupported Uri " + uri);
+        }
+    }
+
+    private int deleteSimRecordsItem(PhonebookArgs args) {
+        validateWritableEf(args, "delete");
+        validateSubscriptionAndEf(args);
+
+        acquireWriteLockOrThrow();
+        try {
+            AdnRecord record = loadRecord(args);
+            if (record == null || record.isEmpty()) {
+                return 0;
+            }
+            if (!updateRecord(args, record, args.pin2, "", "")) {
+                Rlog.e(TAG, "Failed to delete " + args.uri);
+            }
+            notifyChange();
+        } finally {
+            releaseWriteLock();
+        }
+        return 1;
+    }
+
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
+        switch (URI_MATCHER.match(uri)) {
+            case SIM_RECORDS_ITEM:
+                return updateSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras), values);
+            case ELEMENTARY_FILES:
+            case ELEMENTARY_FILES_ITEM:
+            case SIM_RECORDS:
+                throw new UnsupportedOperationException(uri + " does not support update");
+            default:
+                throw new IllegalArgumentException("Unsupported Uri " + uri);
+        }
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        throw new UnsupportedOperationException("Only Update with bundle is supported");
+    }
+
+    private int updateSimRecordsItem(PhonebookArgs args, ContentValues values) {
+        validateWritableEf(args, "update");
+        validateSubscriptionAndEf(args);
+
+        if (values == null || values.isEmpty()) {
+            return 0;
+        }
+        validateValues(args, values);
+        String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
+        String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
+
+        acquireWriteLockOrThrow();
+
+        try {
+            AdnRecord record = loadRecord(args);
+
+            // Note we allow empty records to be updated. This is a bit weird because they are
+            // not returned by query methods but this allows a client application assign a name
+            // to a specific record number. This may be desirable in some phone app use cases since
+            // the record number is often used as a quick dial index.
+            if (record == null) {
+                return 0;
+            }
+            if (!updateRecord(args, record, args.pin2, newName, newPhoneNumber)) {
+                Rlog.e(TAG, "Failed to update " + args.uri);
+                return 0;
+            }
+            notifyChange();
+        } finally {
+            releaseWriteLock();
+        }
+        return 1;
+    }
+
+    void validateSubscriptionAndEf(PhonebookArgs args) {
+        SubscriptionInfo info =
+                args.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                        ? getActiveSubscriptionInfo(args.subscriptionId)
+                        : null;
+        if (info == null) {
+            throw new IllegalArgumentException("No active SIM with subscription ID "
+                    + args.subscriptionId);
+        }
+
+        int[] recordsSize = getRecordsSizeForEf(args);
+        if (recordsSize == null || recordsSize[1] == 0) {
+            throw new IllegalArgumentException(args.efName
+                    + " is not supported for SIM with subscription ID " + args.subscriptionId);
+        }
+    }
+
+    private void acquireWriteLockOrThrow() {
+        try {
+            if (!mWriteLock.tryLock(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Timeout waiting to write");
+            }
+        } catch (InterruptedException e) {
+            throw new IllegalStateException("Write failed");
+        }
+    }
+
+    private void releaseWriteLock() {
+        mWriteLock.unlock();
+    }
+
+    private void validateWritableEf(PhonebookArgs args, String operationName) {
+        if (args.efType == ElementaryFiles.EF_FDN) {
+            if (hasPermissionsForFdnWrite(args)) {
+                return;
+            }
+        }
+        if (args.efType != ElementaryFiles.EF_ADN) {
+            throw new UnsupportedOperationException(
+                    args.uri + " does not support " + operationName);
+        }
+    }
+
+    private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
+        TelephonyManager telephonyManager = getContext().getSystemService(
+                TelephonyManager.class);
+        String callingPackage = getCallingPackage();
+        int granted = PackageManager.PERMISSION_DENIED;
+        if (callingPackage != null) {
+            granted = getContext().getPackageManager().checkPermission(
+                    Manifest.permission.MODIFY_PHONE_STATE, callingPackage);
+        }
+        return granted == PackageManager.PERMISSION_GRANTED
+                || telephonyManager.hasCarrierPrivileges(args.subscriptionId);
+
+    }
+
+
+    private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2,
+            String newName, String newPhone) {
+        try {
+            return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber(
+                    args.subscriptionId, existingRecord.getEfid(), newName, newPhone,
+                    existingRecord.getRecId(),
+                    pin2);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    private SimRecords.NameValidationResult validateName(
+            PhonebookArgs args, @Nullable String name) {
+        name = Strings.nullToEmpty(name);
+        int recordSize = getRecordSize(getRecordsSizeForEf(args));
+        // Validating the name consists of encoding the record in the binary format that it is
+        // stored on the SIM then decoding it and checking whether the decoded name is the same.
+        // The AOSP implementation of AdnRecord replaces unsupported characters with spaces during
+        // encoding.
+        // TODO: It would be good to update AdnRecord to support UCS-2 on the encode path (it
+        //  supports it on the decode path). Right now it's not supported and so any non-latin
+        //  characters will not be valid (at least in the AOSP implementation).
+        byte[] encodedName = AdnRecord.encodeAlphaTag(name);
+        String sanitizedName = AdnRecord.decodeAlphaTag(encodedName, 0, encodedName.length);
+        return new SimRecords.NameValidationResult(name, sanitizedName,
+                encodedName.length, AdnRecord.getMaxAlphaTagBytes(recordSize));
+    }
+
+    private void validatePhoneNumber(@Nullable String phoneNumber) {
+        if (phoneNumber == null || phoneNumber.isEmpty()) {
+            throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is required.");
+        }
+        int actualLength = phoneNumber.length();
+        // When encoded the "+" prefix sets a bit and so doesn't count against the maximum length
+        if (phoneNumber.startsWith("+")) {
+            actualLength--;
+        }
+        if (actualLength > AdnRecord.getMaxPhoneNumberDigits()) {
+            throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is too long.");
+        }
+        for (int i = 0; i < phoneNumber.length(); i++) {
+            char c = phoneNumber.charAt(i);
+            if (!PhoneNumberUtils.isNonSeparator(c)) {
+                throw new IllegalArgumentException(
+                        SimRecords.PHONE_NUMBER + " contains unsupported characters.");
+            }
+        }
+    }
+
+    private void validateValues(PhonebookArgs args, ContentValues values) {
+        if (!SIM_RECORDS_WRITABLE_COLUMNS.containsAll(values.keySet())) {
+            Set<String> unsupportedColumns = new ArraySet<>(values.keySet());
+            unsupportedColumns.removeAll(SIM_RECORDS_WRITABLE_COLUMNS);
+            throw new IllegalArgumentException("Unsupported columns: " + Joiner.on(',')
+                    .join(unsupportedColumns));
+        }
+
+        String phoneNumber = values.getAsString(SimRecords.PHONE_NUMBER);
+        validatePhoneNumber(phoneNumber);
+
+        String name = values.getAsString(SimRecords.NAME);
+        SimRecords.NameValidationResult result = validateName(args, name);
+
+        if (result.getEncodedLength() > result.getMaxEncodedLength()) {
+            throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
+        } else if (!Objects.equals(result.getName(), result.getSanitizedName())) {
+            throw new IllegalArgumentException(
+                    SimRecords.NAME + " contains unsupported characters.");
+        }
+    }
+
+    private List<SubscriptionInfo> getActiveSubscriptionInfoList() {
+        // Getting the SubscriptionInfo requires READ_PHONE_STATE but we're only returning
+        // the subscription ID and slot index which are not sensitive information.
+        CallingIdentity identity = clearCallingIdentity();
+        try {
+            return mSubscriptionManager.getActiveSubscriptionInfoList();
+        } finally {
+            restoreCallingIdentity(identity);
+        }
+    }
+
+    private SubscriptionInfo getActiveSubscriptionInfo(int subId) {
+        // Getting the SubscriptionInfo requires READ_PHONE_STATE.
+        CallingIdentity identity = clearCallingIdentity();
+        try {
+            return mSubscriptionManager.getActiveSubscriptionInfo(subId);
+        } finally {
+            restoreCallingIdentity(identity);
+        }
+    }
+
+    private List<AdnRecord> loadRecordsForEf(PhonebookArgs args) {
+        try {
+            return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber(
+                    args.subscriptionId, args.efid);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    private AdnRecord loadRecord(PhonebookArgs args) {
+        List<AdnRecord> records = loadRecordsForEf(args);
+        if (args.recordNumber > records.size()) {
+            return null;
+        }
+        AdnRecord result = records.get(args.recordNumber - 1);
+        // This should be true but the service could have a different implementation.
+        if (result.getRecId() == args.recordNumber) {
+            return result;
+        }
+        for (AdnRecord record : records) {
+            if (record.getRecId() == args.recordNumber) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+
+    private int[] getRecordsSizeForEf(PhonebookArgs args) {
+        try {
+            return mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
+                    args.subscriptionId, args.efid);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    void notifyChange() {
+        mContentNotifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
+    }
+
+    /** Testable wrapper around {@link ContentResolver#notifyChange(Uri, ContentObserver)} */
+    @TestApi
+    interface ContentNotifier {
+        void notifyChange(Uri uri);
+    }
+
+    /**
+     * Holds the arguments extracted from the Uri and query args for accessing the referenced
+     * phonebook data on a SIM.
+     */
+    private static class PhonebookArgs {
+        public final Uri uri;
+        public final int subscriptionId;
+        public final String efName;
+        public final int efType;
+        public final int efid;
+        public final int recordNumber;
+        public final String pin2;
+
+        PhonebookArgs(Uri uri, int subscriptionId, String efName,
+                @ElementaryFiles.EfType int efType, int efid, int recordNumber,
+                @Nullable Bundle queryArgs) {
+            this.uri = uri;
+            this.subscriptionId = subscriptionId;
+            this.efName = efName;
+            this.efType = efType;
+            this.efid = efid;
+            this.recordNumber = recordNumber;
+            pin2 = efType == ElementaryFiles.EF_FDN && queryArgs != null
+                    ? queryArgs.getString(SimRecords.QUERY_ARG_PIN2)
+                    : null;
+        }
+
+        static PhonebookArgs createFromEfName(Uri uri, int subscriptionId,
+                String efName, int recordNumber, @Nullable Bundle queryArgs) {
+            int efType;
+            int efid;
+            if (efName != null) {
+                switch (efName) {
+                    case ElementaryFiles.EF_ADN_PATH_SEGMENT:
+                        efType = ElementaryFiles.EF_ADN;
+                        efid = IccConstants.EF_ADN;
+                        break;
+                    case ElementaryFiles.EF_FDN_PATH_SEGMENT:
+                        efType = ElementaryFiles.EF_FDN;
+                        efid = IccConstants.EF_FDN;
+                        break;
+                    case ElementaryFiles.EF_SDN_PATH_SEGMENT:
+                        efType = ElementaryFiles.EF_SDN;
+                        efid = IccConstants.EF_SDN;
+                        break;
+                    default:
+                        throw new IllegalArgumentException(
+                                "Unrecognized elementary file " + efName);
+                }
+            } else {
+                efType = ElementaryFiles.EF_UNKNOWN;
+                efid = 0;
+            }
+            return new PhonebookArgs(uri, subscriptionId, efName, efType, efid, recordNumber,
+                    queryArgs);
+        }
+
+        /**
+         * Pattern: elementary_files/subid/${subscriptionId}/${efName}
+         *
+         * e.g. elementary_files/subid/1/adn
+         *
+         * @see ElementaryFiles#getItemUri(int, int)
+         * @see #ELEMENTARY_FILES_ITEM
+         */
+        static PhonebookArgs forElementaryFilesItem(Uri uri) {
+            int subscriptionId = parseSubscriptionIdFromUri(uri, 2);
+            String efName = uri.getPathSegments().get(3);
+            return PhonebookArgs.createFromEfName(
+                    uri, subscriptionId, efName, -1, null);
+        }
+
+        /**
+         * Pattern: subid/${subscriptionId}/${efName}
+         *
+         * <p>e.g. subid/1/adn
+         *
+         * @see SimRecords#getContentUri(int, int)
+         * @see #SIM_RECORDS
+         */
+        static PhonebookArgs forSimRecords(Uri uri, Bundle queryArgs) {
+            int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
+            String efName = uri.getPathSegments().get(2);
+            return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, -1, queryArgs);
+        }
+
+        /**
+         * Pattern: subid/${subscriptionId}/${efName}/${recordNumber}
+         *
+         * <p>e.g. subid/1/adn/10
+         *
+         * @see SimRecords#getItemUri(int, int, int)
+         * @see #SIM_RECORDS_ITEM
+         */
+        static PhonebookArgs forSimRecordsItem(Uri uri, Bundle queryArgs) {
+            int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
+            String efName = uri.getPathSegments().get(2);
+            int recordNumber = parseRecordNumberFromUri(uri, 3);
+            return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, recordNumber,
+                    queryArgs);
+        }
+
+        /**
+         * Pattern: subid/${subscriptionId}/${efName}/validate_name
+         *
+         * @see SimRecords#validateName(ContentResolver, int, int, String)
+         * @see #VALIDATE_NAME
+         */
+        static PhonebookArgs forValidateName(Uri uri, Bundle queryArgs) {
+            int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
+            String efName = uri.getPathSegments().get(2);
+            return PhonebookArgs.createFromEfName(
+                    uri, subscriptionId, efName, -1, queryArgs);
+        }
+
+        private static int parseSubscriptionIdFromUri(Uri uri, int pathIndex) {
+            if (pathIndex == -1) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
+            String segment = uri.getPathSegments().get(pathIndex);
+            try {
+                return Integer.parseInt(segment);
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Invalid subscription ID: " + segment);
+            }
+        }
+
+        private static int parseRecordNumberFromUri(Uri uri, int pathIndex) {
+            try {
+                return Integer.parseInt(uri.getPathSegments().get(pathIndex));
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException(
+                        "Invalid record index: " + uri.getLastPathSegment());
+            }
+        }
+    }
+}