blob: 895286526956e7942bce7b8f63d1a914efd25d09 [file] [log] [blame]
/*
* 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_NUMBER;
import static com.android.internal.telephony.IccProvider.STR_NEW_TAG;
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.SparseArray;
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.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
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_COLUMNS_SET =
ImmutableSet.copyOf(SIM_RECORDS_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;
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<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 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<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);
if (projection == null) {
projection = ELEMENTARY_FILES_ALL_COLUMNS;
}
MatrixCursor result = new MatrixCursor(projection);
try {
SubscriptionInfo info = getActiveSubscriptionInfo(args.subscriptionId);
if (info != null) {
addEfToCursor(result, info, 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;
}
int efid = efIdForEfType(efType);
// Have to load the existing records to get the size because there may be more than one
// phonebook set in which case the total capacity is the sum of the capacity of EF_ADN for
// all the phonebook sets whereas the recordsSize is just the size for a single EF.
List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get()
.getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid);
if (existingRecords == null) {
existingRecords = ImmutableList.of();
}
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, existingRecords.size())
.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 nonEmptyCount = 0;
for (AdnRecord record : existingRecords) {
if (!record.isEmpty()) {
nonEmptyCount++;
}
}
row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount);
}
}
private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
validateProjection(SIM_RECORDS_COLUMNS_SET, 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());
SparseArray<MatrixCursor.RowBuilder> rowBuilders = new SparseArray<>(records.size());
for (int i = 0; i < records.size(); i++) {
AdnRecord record = records.get(i);
if (!record.isEmpty()) {
rowBuilders.put(i, 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 (int i = 0; i < rowBuilders.size(); i++) {
rowBuilders.valueAt(i).add(args.subscriptionId);
}
break;
case SimRecords.ELEMENTARY_FILE_TYPE:
for (int i = 0; i < rowBuilders.size(); i++) {
rowBuilders.valueAt(i).add(args.efType);
}
break;
case SimRecords.RECORD_NUMBER:
for (int i = 0; i < rowBuilders.size(); i++) {
int index = rowBuilders.keyAt(i);
MatrixCursor.RowBuilder rowBuilder = rowBuilders.valueAt(i);
// See b/201685690. The logical record number, i.e. the 1-based index in the
// list, is used the rather than AdnRecord.getRecId() because getRecId is
// not offset when a single logical EF is made up of multiple physical EFs.
rowBuilder.add(index + 1);
}
break;
case SimRecords.NAME:
for (int i = 0; i < rowBuilders.size(); i++) {
AdnRecord record = records.get(rowBuilders.keyAt(i));
rowBuilders.valueAt(i).add(record.getAlphaTag());
}
break;
case SimRecords.PHONE_NUMBER:
for (int i = 0; i < rowBuilders.size(); i++) {
AdnRecord record = records.get(rowBuilders.keyAt(i));
rowBuilders.valueAt(i).add(record.getNumber());
}
break;
default:
Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
break;
}
}
return result;
}
private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
validateProjection(SIM_RECORDS_COLUMNS_SET, 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<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 = 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<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);
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<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);
}
}
@Nullable
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 (records == null || args.recordNumber > records.size()) {
return null;
}
return records.get(args.recordNumber - 1);
}
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}
*
* <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);
}
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());
}
}
}
}