/* * 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 com.android.internal.telephony.IccProvider.STR_NEW_TAG; import static com.android.internal.telephony.IccProvider.STR_NEW_NUMBER; 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 ELEMENTARY_FILES_COLUMNS_SET = ImmutableSet.copyOf(ELEMENTARY_FILES_ALL_COLUMNS); private static final Set 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; 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); } // 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 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 allowed, String[] projection) { if (projection == null || allowed.containsAll(Arrays.asList(projection))) { return; } Set 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 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 Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { if (SimRecords.GET_ENCODED_NAME_LENGTH_METHOD_NAME.equals(method)) { // No permissions checks needed. This isn't leaking any sensitive information since the // name we are checking is provided by the caller. return callForEncodedNameLength(arg); } return super.call(method, arg, extras); } private Bundle callForEncodedNameLength(String name) { Bundle result = new Bundle(); result.putInt(SimRecords.EXTRA_ENCODED_NAME_LENGTH, getEncodedNameLength(name)); return result; } private int getEncodedNameLength(String name) { if (Strings.isNullOrEmpty(name)) { return 0; } else { byte[] encoded = AdnRecord.encodeAlphaTag(name); return encoded.length; } } @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); 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 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); if (projection == null) { projection = ELEMENTARY_FILES_ALL_COLUMNS; } 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 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 records = loadRecordsForEf(args); if (records == null) { return new MatrixCursor(projection, 0); } MatrixCursor result = new MatrixCursor(projection, records.size()); List> 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 row : rowBuilders) { row.second.add(args.subscriptionId); } break; case SimRecords.ELEMENTARY_FILE_TYPE: for (Pair row : rowBuilders) { row.second.add(args.efType); } break; case SimRecords.RECORD_NUMBER: for (Pair row : rowBuilders) { row.second.add(row.first.getRecId()); } break; case SimRecords.NAME: for (Pair row : rowBuilders) { row.second.add(row.first.getAlphaTag()); } break; case SimRecords.PHONE_NUMBER: for (Pair 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; } @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 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 = Objects.requireNonNull( 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 { ContentValues values = new ContentValues(); values.put(STR_NEW_TAG, newName); values.put(STR_NEW_NUMBER, newPhone); return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber( args.subscriptionId, existingRecord.getEfid(), values, existingRecord.getRecId(), pin2); } catch (RemoteException e) { return false; } } 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 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); int length = getEncodedNameLength(name); int[] recordsSize = getRecordsSizeForEf(args); if (recordsSize == null) { throw new IllegalStateException( "Failed to get " + ElementaryFiles.NAME_MAX_LENGTH + " from SIM"); } int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)); if (length > maxLength) { throw new IllegalArgumentException(SimRecords.NAME + " is too long."); } } private List 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 loadRecordsForEf(PhonebookArgs args) { try { return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber( args.subscriptionId, args.efid); } catch (RemoteException e) { return null; } } private AdnRecord loadRecord(PhonebookArgs args) { List records = loadRecordsForEf(args); if (records == null || 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.PATH_SEGMENT_EF_ADN: efType = ElementaryFiles.EF_ADN; efid = IccConstants.EF_ADN; break; case ElementaryFiles.PATH_SEGMENT_EF_FDN: efType = ElementaryFiles.EF_FDN; efid = IccConstants.EF_FDN; break; case ElementaryFiles.PATH_SEGMENT_EF_SDN: 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} * *

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} * *

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); } 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()); } } } }