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