• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.phone;
18 
19 import static com.android.internal.telephony.IccProvider.STR_NEW_NUMBER;
20 import static com.android.internal.telephony.IccProvider.STR_NEW_TAG;
21 
22 import android.Manifest;
23 import android.annotation.TestApi;
24 import android.content.ContentProvider;
25 import android.content.ContentResolver;
26 import android.content.ContentValues;
27 import android.content.UriMatcher;
28 import android.content.pm.PackageManager;
29 import android.database.ContentObserver;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.net.Uri;
33 import android.os.Binder;
34 import android.os.Bundle;
35 import android.os.CancellationSignal;
36 import android.os.RemoteException;
37 import android.provider.SimPhonebookContract;
38 import android.provider.SimPhonebookContract.ElementaryFiles;
39 import android.provider.SimPhonebookContract.SimRecords;
40 import android.telephony.PhoneNumberUtils;
41 import android.telephony.Rlog;
42 import android.telephony.SubscriptionInfo;
43 import android.telephony.SubscriptionManager;
44 import android.telephony.TelephonyFrameworkInitializer;
45 import android.telephony.TelephonyManager;
46 import android.util.ArraySet;
47 import android.util.SparseArray;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 
52 import com.android.internal.annotations.VisibleForTesting;
53 import com.android.internal.telephony.IIccPhoneBook;
54 import com.android.internal.telephony.flags.Flags;
55 import com.android.internal.telephony.uicc.AdnRecord;
56 import com.android.internal.telephony.uicc.IccConstants;
57 
58 import com.google.common.base.Joiner;
59 import com.google.common.base.Strings;
60 import com.google.common.collect.ImmutableList;
61 import com.google.common.collect.ImmutableSet;
62 import com.google.common.util.concurrent.MoreExecutors;
63 
64 import java.util.Arrays;
65 import java.util.LinkedHashSet;
66 import java.util.List;
67 import java.util.Objects;
68 import java.util.Set;
69 import java.util.concurrent.TimeUnit;
70 import java.util.concurrent.locks.Lock;
71 import java.util.concurrent.locks.ReentrantLock;
72 import java.util.function.Supplier;
73 
74 /**
75  * Provider for contact records stored on the SIM card.
76  *
77  * @see SimPhonebookContract
78  */
79 public class SimPhonebookProvider extends ContentProvider {
80 
81     @VisibleForTesting
82     static final String[] ELEMENTARY_FILES_ALL_COLUMNS = {
83             ElementaryFiles.SLOT_INDEX,
84             ElementaryFiles.SUBSCRIPTION_ID,
85             ElementaryFiles.EF_TYPE,
86             ElementaryFiles.MAX_RECORDS,
87             ElementaryFiles.RECORD_COUNT,
88             ElementaryFiles.NAME_MAX_LENGTH,
89             ElementaryFiles.PHONE_NUMBER_MAX_LENGTH
90     };
91     @VisibleForTesting
92     static final String[] SIM_RECORDS_ALL_COLUMNS = {
93             SimRecords.SUBSCRIPTION_ID,
94             SimRecords.ELEMENTARY_FILE_TYPE,
95             SimRecords.RECORD_NUMBER,
96             SimRecords.NAME,
97             SimRecords.PHONE_NUMBER
98     };
99     private static final String TAG = "SimPhonebookProvider";
100     private static final Set<String> ELEMENTARY_FILES_COLUMNS_SET =
101             ImmutableSet.copyOf(ELEMENTARY_FILES_ALL_COLUMNS);
102     private static final Set<String> SIM_RECORDS_COLUMNS_SET =
103             ImmutableSet.copyOf(SIM_RECORDS_ALL_COLUMNS);
104     private static final Set<String> SIM_RECORDS_WRITABLE_COLUMNS = ImmutableSet.of(
105             SimRecords.NAME, SimRecords.PHONE_NUMBER
106     );
107 
108     private static final int WRITE_TIMEOUT_SECONDS = 30;
109 
110     private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
111 
112     private static final int ELEMENTARY_FILES = 100;
113     private static final int ELEMENTARY_FILES_ITEM = 101;
114     private static final int SIM_RECORDS = 200;
115     private static final int SIM_RECORDS_ITEM = 201;
116 
117     static {
URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES)118         URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
119                 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)120         URI_MATCHER.addURI(
121                 SimPhonebookContract.AUTHORITY,
122                 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/"
123                         + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*",
124                 ELEMENTARY_FILES_ITEM);
URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS)125         URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
126                 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS);
URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM)127         URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
128                 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM);
129     }
130 
131     // Only allow 1 write at a time to prevent races; the mutations are based on reads of the
132     // existing list of records which means concurrent writes would be problematic.
133     private final Lock mWriteLock = new ReentrantLock(true);
134     private SubscriptionManager mSubscriptionManager;
135     private Supplier<IIccPhoneBook> mIccPhoneBookSupplier;
136     private ContentNotifier mContentNotifier;
137 
efIdForEfType(@lementaryFiles.EfType int efType)138     static int efIdForEfType(@ElementaryFiles.EfType int efType) {
139         switch (efType) {
140             case ElementaryFiles.EF_ADN:
141                 return IccConstants.EF_ADN;
142             case ElementaryFiles.EF_FDN:
143                 return IccConstants.EF_FDN;
144             case ElementaryFiles.EF_SDN:
145                 return IccConstants.EF_SDN;
146             default:
147                 return 0;
148         }
149     }
150 
validateProjection(Set<String> allowed, String[] projection)151     private static void validateProjection(Set<String> allowed, String[] projection) {
152         if (projection == null || allowed.containsAll(Arrays.asList(projection))) {
153             return;
154         }
155         Set<String> invalidColumns = new LinkedHashSet<>(Arrays.asList(projection));
156         invalidColumns.removeAll(allowed);
157         throw new IllegalArgumentException(
158                 "Unsupported columns: " + Joiner.on(",").join(invalidColumns));
159     }
160 
getRecordSize(int[] recordsSize)161     private static int getRecordSize(int[] recordsSize) {
162         return recordsSize[0];
163     }
164 
getRecordCount(int[] recordsSize)165     private static int getRecordCount(int[] recordsSize) {
166         return recordsSize[2];
167     }
168 
169     /** Returns the IccPhoneBook used to load the AdnRecords. */
getIccPhoneBook()170     private static IIccPhoneBook getIccPhoneBook() {
171         return IIccPhoneBook.Stub.asInterface(TelephonyFrameworkInitializer
172                 .getTelephonyServiceManager().getIccPhoneBookServiceRegisterer().get());
173     }
174 
175     @Override
onCreate()176     public boolean onCreate() {
177         ContentResolver resolver = getContext().getContentResolver();
178 
179         SubscriptionManager sm = getContext().getSystemService(SubscriptionManager.class);
180         if (sm == null) {
181             return false;
182         } else if (Flags.workProfileApiSplit()) {
183             sm = sm.createForAllUserProfiles();
184         }
185         return onCreate(sm,
186                 SimPhonebookProvider::getIccPhoneBook,
187                 uri -> resolver.notifyChange(uri, null));
188     }
189 
190     @TestApi
onCreate(@onNull SubscriptionManager subscriptionManager, Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier)191     boolean onCreate(@NonNull SubscriptionManager subscriptionManager,
192             Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier) {
193         mSubscriptionManager = subscriptionManager;
194         mIccPhoneBookSupplier = iccPhoneBookSupplier;
195         mContentNotifier = notifier;
196 
197         mSubscriptionManager.addOnSubscriptionsChangedListener(MoreExecutors.directExecutor(),
198                 new SubscriptionManager.OnSubscriptionsChangedListener() {
199                     boolean mFirstCallback = true;
200                     private int[] mNotifiedSubIds = {};
201 
202                     @Override
203                     public void onSubscriptionsChanged() {
204                         if (mFirstCallback) {
205                             mFirstCallback = false;
206                             return;
207                         }
208                         int[] activeSubIds = mSubscriptionManager.getActiveSubscriptionIdList();
209                         if (!Arrays.equals(mNotifiedSubIds, activeSubIds)) {
210                             notifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
211                             mNotifiedSubIds = Arrays.copyOf(activeSubIds, activeSubIds.length);
212                         }
213                     }
214                 });
215         return true;
216     }
217 
218     @Nullable
219     @Override
call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)220     public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
221         if (SimRecords.GET_ENCODED_NAME_LENGTH_METHOD_NAME.equals(method)) {
222             // No permissions checks needed. This isn't leaking any sensitive information since the
223             // name we are checking is provided by the caller.
224             return callForEncodedNameLength(arg);
225         }
226         return super.call(method, arg, extras);
227     }
228 
callForEncodedNameLength(String name)229     private Bundle callForEncodedNameLength(String name) {
230         Bundle result = new Bundle();
231         result.putInt(SimRecords.EXTRA_ENCODED_NAME_LENGTH, getEncodedNameLength(name));
232         return result;
233     }
234 
getEncodedNameLength(String name)235     private int getEncodedNameLength(String name) {
236         if (Strings.isNullOrEmpty(name)) {
237             return 0;
238         } else {
239             byte[] encoded = AdnRecord.encodeAlphaTag(name);
240             return encoded.length;
241         }
242     }
243 
244     @Nullable
245     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)246     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs,
247             @Nullable CancellationSignal cancellationSignal) {
248         if (queryArgs != null && (queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION)
249                 || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS)
250                 || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_LIMIT))) {
251             throw new IllegalArgumentException(
252                     "A SQL selection was provided but it is not supported by this provider.");
253         }
254         switch (URI_MATCHER.match(uri)) {
255             case ELEMENTARY_FILES:
256                 return queryElementaryFiles(projection);
257             case ELEMENTARY_FILES_ITEM:
258                 return queryElementaryFilesItem(PhonebookArgs.forElementaryFilesItem(uri),
259                         projection);
260             case SIM_RECORDS:
261                 return querySimRecords(PhonebookArgs.forSimRecords(uri, queryArgs), projection);
262             case SIM_RECORDS_ITEM:
263                 return querySimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, queryArgs),
264                         projection);
265             default:
266                 throw new IllegalArgumentException("Unsupported Uri " + uri);
267         }
268     }
269 
270     @Nullable
271     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)272     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
273             @Nullable String[] selectionArgs, @Nullable String sortOrder,
274             @Nullable CancellationSignal cancellationSignal) {
275         throw new UnsupportedOperationException("Only query with Bundle is supported");
276     }
277 
278     @Nullable
279     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)280     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
281             @Nullable String[] selectionArgs, @Nullable String sortOrder) {
282         throw new UnsupportedOperationException("Only query with Bundle is supported");
283     }
284 
queryElementaryFiles(String[] projection)285     private Cursor queryElementaryFiles(String[] projection) {
286         validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
287         if (projection == null) {
288             projection = ELEMENTARY_FILES_ALL_COLUMNS;
289         }
290 
291         MatrixCursor result = new MatrixCursor(projection);
292 
293         List<SubscriptionInfo> activeSubscriptions = getActiveSubscriptionInfoList();
294         for (SubscriptionInfo subInfo : activeSubscriptions) {
295             try {
296                 addEfToCursor(result, subInfo, ElementaryFiles.EF_ADN);
297                 addEfToCursor(result, subInfo, ElementaryFiles.EF_FDN);
298                 addEfToCursor(result, subInfo, ElementaryFiles.EF_SDN);
299             } catch (RemoteException e) {
300                 // Return an empty cursor. If service to access it is throwing remote
301                 // exceptions then it's basically the same as not having a SIM.
302                 return new MatrixCursor(projection, 0);
303             }
304         }
305         return result;
306     }
307 
queryElementaryFilesItem(PhonebookArgs args, String[] projection)308     private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) {
309         validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
310         if (projection == null) {
311             projection = ELEMENTARY_FILES_ALL_COLUMNS;
312         }
313 
314         MatrixCursor result = new MatrixCursor(projection);
315         try {
316             SubscriptionInfo info = getActiveSubscriptionInfo(args.subscriptionId);
317             if (info != null) {
318                 addEfToCursor(result, info, args.efType);
319             }
320         } catch (RemoteException e) {
321             // Return an empty cursor. If service to access it is throwing remote
322             // exceptions then it's basically the same as not having a SIM.
323             return new MatrixCursor(projection, 0);
324         }
325         return result;
326     }
327 
addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType)328     private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
329             int efType) throws RemoteException {
330         int[] recordsSize = mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
331                 subscriptionInfo.getSubscriptionId(), efIdForEfType(efType));
332         addEfToCursor(result, subscriptionInfo, efType, recordsSize);
333     }
334 
addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType, int[] recordsSize)335     private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
336             int efType, int[] recordsSize) throws RemoteException {
337         // If the record count is zero then the SIM doesn't support the elementary file so just
338         // omit it.
339         if (recordsSize == null || getRecordCount(recordsSize) == 0) {
340             return;
341         }
342         int efid = efIdForEfType(efType);
343         // Have to load the existing records to get the size because there may be more than one
344         // phonebook set in which case the total capacity is the sum of the capacity of EF_ADN for
345         // all the phonebook sets whereas the recordsSize is just the size for a single EF.
346         List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get()
347                 .getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid);
348         if (existingRecords == null) {
349             existingRecords = ImmutableList.of();
350         }
351         MatrixCursor.RowBuilder row = result.newRow()
352                 .add(ElementaryFiles.SLOT_INDEX, subscriptionInfo.getSimSlotIndex())
353                 .add(ElementaryFiles.SUBSCRIPTION_ID, subscriptionInfo.getSubscriptionId())
354                 .add(ElementaryFiles.EF_TYPE, efType)
355                 .add(ElementaryFiles.MAX_RECORDS, existingRecords.size())
356                 .add(ElementaryFiles.NAME_MAX_LENGTH,
357                         AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)))
358                 .add(ElementaryFiles.PHONE_NUMBER_MAX_LENGTH,
359                         AdnRecord.getMaxPhoneNumberDigits());
360         if (result.getColumnIndex(ElementaryFiles.RECORD_COUNT) != -1) {
361             int nonEmptyCount = 0;
362             for (AdnRecord record : existingRecords) {
363                 if (!record.isEmpty()) {
364                     nonEmptyCount++;
365                 }
366             }
367             row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount);
368         }
369     }
370 
querySimRecords(PhonebookArgs args, String[] projection)371     private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
372         validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
373         validateSubscriptionAndEf(args);
374         if (projection == null) {
375             projection = SIM_RECORDS_ALL_COLUMNS;
376         }
377 
378         List<AdnRecord> records = loadRecordsForEf(args);
379         if (records == null) {
380             return new MatrixCursor(projection, 0);
381         }
382         MatrixCursor result = new MatrixCursor(projection, records.size());
383         SparseArray<MatrixCursor.RowBuilder> rowBuilders = new SparseArray<>(records.size());
384         for (int i = 0; i < records.size(); i++) {
385             AdnRecord record = records.get(i);
386             if (!record.isEmpty()) {
387                 rowBuilders.put(i, result.newRow());
388             }
389         }
390         // This is kind of ugly but avoids looking up columns in an inner loop.
391         for (String column : projection) {
392             switch (column) {
393                 case SimRecords.SUBSCRIPTION_ID:
394                     for (int i = 0; i < rowBuilders.size(); i++) {
395                         rowBuilders.valueAt(i).add(args.subscriptionId);
396                     }
397                     break;
398                 case SimRecords.ELEMENTARY_FILE_TYPE:
399                     for (int i = 0; i < rowBuilders.size(); i++) {
400                         rowBuilders.valueAt(i).add(args.efType);
401                     }
402                     break;
403                 case SimRecords.RECORD_NUMBER:
404                     for (int i = 0; i < rowBuilders.size(); i++) {
405                         int index = rowBuilders.keyAt(i);
406                         MatrixCursor.RowBuilder rowBuilder = rowBuilders.valueAt(i);
407                         // See b/201685690. The logical record number, i.e. the 1-based index in the
408                         // list, is used the rather than AdnRecord.getRecId() because getRecId is
409                         // not offset when a single logical EF is made up of multiple physical EFs.
410                         rowBuilder.add(index + 1);
411                     }
412                     break;
413                 case SimRecords.NAME:
414                     for (int i = 0; i < rowBuilders.size(); i++) {
415                         AdnRecord record = records.get(rowBuilders.keyAt(i));
416                         rowBuilders.valueAt(i).add(record.getAlphaTag());
417                     }
418                     break;
419                 case SimRecords.PHONE_NUMBER:
420                     for (int i = 0; i < rowBuilders.size(); i++) {
421                         AdnRecord record = records.get(rowBuilders.keyAt(i));
422                         rowBuilders.valueAt(i).add(record.getNumber());
423                     }
424                     break;
425                 default:
426                     Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
427                     break;
428             }
429         }
430         return result;
431     }
432 
querySimRecordsItem(PhonebookArgs args, String[] projection)433     private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
434         validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
435         if (projection == null) {
436             projection = SIM_RECORDS_ALL_COLUMNS;
437         }
438         validateSubscriptionAndEf(args);
439         AdnRecord record = loadRecord(args);
440 
441         MatrixCursor result = new MatrixCursor(projection, 1);
442         if (record == null || record.isEmpty()) {
443             return result;
444         }
445         result.newRow()
446                 .add(SimRecords.SUBSCRIPTION_ID, args.subscriptionId)
447                 .add(SimRecords.ELEMENTARY_FILE_TYPE, args.efType)
448                 .add(SimRecords.RECORD_NUMBER, record.getRecId())
449                 .add(SimRecords.NAME, record.getAlphaTag())
450                 .add(SimRecords.PHONE_NUMBER, record.getNumber());
451         return result;
452     }
453 
454     @Nullable
455     @Override
getType(@onNull Uri uri)456     public String getType(@NonNull Uri uri) {
457         switch (URI_MATCHER.match(uri)) {
458             case ELEMENTARY_FILES:
459                 return ElementaryFiles.CONTENT_TYPE;
460             case ELEMENTARY_FILES_ITEM:
461                 return ElementaryFiles.CONTENT_ITEM_TYPE;
462             case SIM_RECORDS:
463                 return SimRecords.CONTENT_TYPE;
464             case SIM_RECORDS_ITEM:
465                 return SimRecords.CONTENT_ITEM_TYPE;
466             default:
467                 throw new IllegalArgumentException("Unsupported Uri " + uri);
468         }
469     }
470 
471     @Nullable
472     @Override
insert(@onNull Uri uri, @Nullable ContentValues values)473     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
474         return insert(uri, values, null);
475     }
476 
477     @Nullable
478     @Override
insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)479     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
480         switch (URI_MATCHER.match(uri)) {
481             case SIM_RECORDS:
482                 return insertSimRecord(PhonebookArgs.forSimRecords(uri, extras), values);
483             case ELEMENTARY_FILES:
484             case ELEMENTARY_FILES_ITEM:
485             case SIM_RECORDS_ITEM:
486                 throw new UnsupportedOperationException(uri + " does not support insert");
487             default:
488                 throw new IllegalArgumentException("Unsupported Uri " + uri);
489         }
490     }
491 
insertSimRecord(PhonebookArgs args, ContentValues values)492     private Uri insertSimRecord(PhonebookArgs args, ContentValues values) {
493         validateWritableEf(args, "insert");
494         validateSubscriptionAndEf(args);
495 
496         if (values == null || values.isEmpty()) {
497             return null;
498         }
499         validateValues(args, values);
500         String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
501         String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
502 
503         acquireWriteLockOrThrow();
504         try {
505             List<AdnRecord> records = loadRecordsForEf(args);
506             if (records == null) {
507                 Rlog.e(TAG, "Failed to load existing records for " + args.uri);
508                 return null;
509             }
510             AdnRecord emptyRecord = null;
511             for (AdnRecord record : records) {
512                 if (record.isEmpty()) {
513                     emptyRecord = record;
514                     break;
515                 }
516             }
517             if (emptyRecord == null) {
518                 // When there are no empty records that means the EF is full.
519                 throw new IllegalStateException(
520                         args.uri + " is full. Please delete records to add new ones.");
521             }
522             boolean success = updateRecord(args, emptyRecord, args.pin2, newName, newPhoneNumber);
523             if (!success) {
524                 Rlog.e(TAG, "Insert failed for " + args.uri);
525                 // Something didn't work but since we don't have any more specific
526                 // information to provide to the caller it's better to just return null
527                 // rather than throwing and possibly crashing their process.
528                 return null;
529             }
530             notifyChange();
531             return SimRecords.getItemUri(args.subscriptionId, args.efType, emptyRecord.getRecId());
532         } finally {
533             releaseWriteLock();
534         }
535     }
536 
537     @Override
delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)538     public int delete(@NonNull Uri uri, @Nullable String selection,
539             @Nullable String[] selectionArgs) {
540         throw new UnsupportedOperationException("Only delete with Bundle is supported");
541     }
542 
543     @Override
delete(@onNull Uri uri, @Nullable Bundle extras)544     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
545         switch (URI_MATCHER.match(uri)) {
546             case SIM_RECORDS_ITEM:
547                 return deleteSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras));
548             case ELEMENTARY_FILES:
549             case ELEMENTARY_FILES_ITEM:
550             case SIM_RECORDS:
551                 throw new UnsupportedOperationException(uri + " does not support delete");
552             default:
553                 throw new IllegalArgumentException("Unsupported Uri " + uri);
554         }
555     }
556 
deleteSimRecordsItem(PhonebookArgs args)557     private int deleteSimRecordsItem(PhonebookArgs args) {
558         validateWritableEf(args, "delete");
559         validateSubscriptionAndEf(args);
560 
561         acquireWriteLockOrThrow();
562         try {
563             AdnRecord record = loadRecord(args);
564             if (record == null || record.isEmpty()) {
565                 return 0;
566             }
567             if (!updateRecord(args, record, args.pin2, "", "")) {
568                 Rlog.e(TAG, "Failed to delete " + args.uri);
569             }
570             notifyChange();
571         } finally {
572             releaseWriteLock();
573         }
574         return 1;
575     }
576 
577 
578     @Override
update(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)579     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
580         switch (URI_MATCHER.match(uri)) {
581             case SIM_RECORDS_ITEM:
582                 return updateSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras), values);
583             case ELEMENTARY_FILES:
584             case ELEMENTARY_FILES_ITEM:
585             case SIM_RECORDS:
586                 throw new UnsupportedOperationException(uri + " does not support update");
587             default:
588                 throw new IllegalArgumentException("Unsupported Uri " + uri);
589         }
590     }
591 
592     @Override
update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)593     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
594             @Nullable String[] selectionArgs) {
595         throw new UnsupportedOperationException("Only Update with bundle is supported");
596     }
597 
updateSimRecordsItem(PhonebookArgs args, ContentValues values)598     private int updateSimRecordsItem(PhonebookArgs args, ContentValues values) {
599         validateWritableEf(args, "update");
600         validateSubscriptionAndEf(args);
601 
602         if (values == null || values.isEmpty()) {
603             return 0;
604         }
605         validateValues(args, values);
606         String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
607         String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
608 
609         acquireWriteLockOrThrow();
610 
611         try {
612             AdnRecord record = loadRecord(args);
613 
614             // Note we allow empty records to be updated. This is a bit weird because they are
615             // not returned by query methods but this allows a client application assign a name
616             // to a specific record number. This may be desirable in some phone app use cases since
617             // the record number is often used as a quick dial index.
618             if (record == null) {
619                 return 0;
620             }
621             if (!updateRecord(args, record, args.pin2, newName, newPhoneNumber)) {
622                 Rlog.e(TAG, "Failed to update " + args.uri);
623                 return 0;
624             }
625             notifyChange();
626         } finally {
627             releaseWriteLock();
628         }
629         return 1;
630     }
631 
validateSubscriptionAndEf(PhonebookArgs args)632     void validateSubscriptionAndEf(PhonebookArgs args) {
633         SubscriptionInfo info =
634                 args.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
635                         ? getActiveSubscriptionInfo(args.subscriptionId)
636                         : null;
637         if (info == null) {
638             throw new IllegalArgumentException("No active SIM with subscription ID "
639                     + args.subscriptionId);
640         }
641 
642         int[] recordsSize = getRecordsSizeForEf(args);
643         if (recordsSize == null || recordsSize[1] == 0) {
644             throw new IllegalArgumentException(args.efName
645                     + " is not supported for SIM with subscription ID " + args.subscriptionId);
646         }
647     }
648 
acquireWriteLockOrThrow()649     private void acquireWriteLockOrThrow() {
650         try {
651             if (!mWriteLock.tryLock(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
652                 throw new IllegalStateException("Timeout waiting to write");
653             }
654         } catch (InterruptedException e) {
655             throw new IllegalStateException("Write failed");
656         }
657     }
658 
releaseWriteLock()659     private void releaseWriteLock() {
660         mWriteLock.unlock();
661     }
662 
validateWritableEf(PhonebookArgs args, String operationName)663     private void validateWritableEf(PhonebookArgs args, String operationName) {
664         if (args.efType == ElementaryFiles.EF_FDN) {
665             if (hasPermissionsForFdnWrite(args)) {
666                 return;
667             }
668         }
669         if (args.efType != ElementaryFiles.EF_ADN) {
670             throw new UnsupportedOperationException(
671                     args.uri + " does not support " + operationName);
672         }
673     }
674 
hasPermissionsForFdnWrite(PhonebookArgs args)675     private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
676         TelephonyManager telephonyManager = Objects.requireNonNull(
677                 getContext().getSystemService(TelephonyManager.class));
678         String callingPackage = getCallingPackage();
679         int granted = PackageManager.PERMISSION_DENIED;
680         if (callingPackage != null) {
681             if (Flags.hsumPackageManager()) {
682                 granted = getContext().createContextAsUser(Binder.getCallingUserHandle(), 0)
683                         .getPackageManager().checkPermission(
684                                 Manifest.permission.MODIFY_PHONE_STATE, callingPackage);
685             } else {
686                 granted = getContext().getPackageManager().checkPermission(
687                         Manifest.permission.MODIFY_PHONE_STATE, callingPackage);
688             }
689         }
690         return granted == PackageManager.PERMISSION_GRANTED
691                 || telephonyManager.hasCarrierPrivileges(args.subscriptionId);
692 
693     }
694 
695 
updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, String newName, String newPhone)696     private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2,
697             String newName, String newPhone) {
698         try {
699             ContentValues values = new ContentValues();
700             values.put(STR_NEW_TAG, newName);
701             values.put(STR_NEW_NUMBER, newPhone);
702             return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber(
703                     args.subscriptionId, existingRecord.getEfid(), values,
704                     existingRecord.getRecId(),
705                     pin2);
706         } catch (RemoteException e) {
707             return false;
708         }
709     }
710 
validatePhoneNumber(@ullable String phoneNumber)711     private void validatePhoneNumber(@Nullable String phoneNumber) {
712         if (phoneNumber == null || phoneNumber.isEmpty()) {
713             throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is required.");
714         }
715         int actualLength = phoneNumber.length();
716         // When encoded the "+" prefix sets a bit and so doesn't count against the maximum length
717         if (phoneNumber.startsWith("+")) {
718             actualLength--;
719         }
720         if (actualLength > AdnRecord.getMaxPhoneNumberDigits()) {
721             throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is too long.");
722         }
723         for (int i = 0; i < phoneNumber.length(); i++) {
724             char c = phoneNumber.charAt(i);
725             if (!PhoneNumberUtils.isNonSeparator(c)) {
726                 throw new IllegalArgumentException(
727                         SimRecords.PHONE_NUMBER + " contains unsupported characters.");
728             }
729         }
730     }
731 
validateValues(PhonebookArgs args, ContentValues values)732     private void validateValues(PhonebookArgs args, ContentValues values) {
733         if (!SIM_RECORDS_WRITABLE_COLUMNS.containsAll(values.keySet())) {
734             Set<String> unsupportedColumns = new ArraySet<>(values.keySet());
735             unsupportedColumns.removeAll(SIM_RECORDS_WRITABLE_COLUMNS);
736             throw new IllegalArgumentException("Unsupported columns: " + Joiner.on(',')
737                     .join(unsupportedColumns));
738         }
739 
740         String phoneNumber = values.getAsString(SimRecords.PHONE_NUMBER);
741         validatePhoneNumber(phoneNumber);
742 
743         String name = values.getAsString(SimRecords.NAME);
744         int length = getEncodedNameLength(name);
745         int[] recordsSize = getRecordsSizeForEf(args);
746         if (recordsSize == null) {
747             throw new IllegalStateException(
748                     "Failed to get " + ElementaryFiles.NAME_MAX_LENGTH + " from SIM");
749         }
750         int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize));
751 
752         if (length > maxLength) {
753             throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
754         }
755     }
756 
getActiveSubscriptionInfoList()757     private List<SubscriptionInfo> getActiveSubscriptionInfoList() {
758         // Getting the SubscriptionInfo requires READ_PHONE_STATE but we're only returning
759         // the subscription ID and slot index which are not sensitive information.
760         CallingIdentity identity = clearCallingIdentity();
761         try {
762             return mSubscriptionManager.getActiveSubscriptionInfoList();
763         } finally {
764             restoreCallingIdentity(identity);
765         }
766     }
767 
768     @Nullable
getActiveSubscriptionInfo(int subId)769     private SubscriptionInfo getActiveSubscriptionInfo(int subId) {
770         // Getting the SubscriptionInfo requires READ_PHONE_STATE.
771         CallingIdentity identity = clearCallingIdentity();
772         try {
773             return mSubscriptionManager.getActiveSubscriptionInfo(subId);
774         } finally {
775             restoreCallingIdentity(identity);
776         }
777     }
778 
loadRecordsForEf(PhonebookArgs args)779     private List<AdnRecord> loadRecordsForEf(PhonebookArgs args) {
780         try {
781             return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber(
782                     args.subscriptionId, args.efid);
783         } catch (RemoteException e) {
784             return null;
785         }
786     }
787 
loadRecord(PhonebookArgs args)788     private AdnRecord loadRecord(PhonebookArgs args) {
789         List<AdnRecord> records = loadRecordsForEf(args);
790         if (records == null || args.recordNumber > records.size()) {
791             return null;
792         }
793         return records.get(args.recordNumber - 1);
794     }
795 
getRecordsSizeForEf(PhonebookArgs args)796     private int[] getRecordsSizeForEf(PhonebookArgs args) {
797         try {
798             return mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
799                     args.subscriptionId, args.efid);
800         } catch (RemoteException e) {
801             return null;
802         }
803     }
804 
notifyChange()805     void notifyChange() {
806         mContentNotifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
807     }
808 
809     /** Testable wrapper around {@link ContentResolver#notifyChange(Uri, ContentObserver)} */
810     @TestApi
811     interface ContentNotifier {
notifyChange(Uri uri)812         void notifyChange(Uri uri);
813     }
814 
815     /**
816      * Holds the arguments extracted from the Uri and query args for accessing the referenced
817      * phonebook data on a SIM.
818      */
819     private static class PhonebookArgs {
820         public final Uri uri;
821         public final int subscriptionId;
822         public final String efName;
823         public final int efType;
824         public final int efid;
825         public final int recordNumber;
826         public final String pin2;
827 
PhonebookArgs(Uri uri, int subscriptionId, String efName, @ElementaryFiles.EfType int efType, int efid, int recordNumber, @Nullable Bundle queryArgs)828         PhonebookArgs(Uri uri, int subscriptionId, String efName,
829                 @ElementaryFiles.EfType int efType, int efid, int recordNumber,
830                 @Nullable Bundle queryArgs) {
831             this.uri = uri;
832             this.subscriptionId = subscriptionId;
833             this.efName = efName;
834             this.efType = efType;
835             this.efid = efid;
836             this.recordNumber = recordNumber;
837             pin2 = efType == ElementaryFiles.EF_FDN && queryArgs != null
838                     ? queryArgs.getString(SimRecords.QUERY_ARG_PIN2)
839                     : null;
840         }
841 
createFromEfName(Uri uri, int subscriptionId, String efName, int recordNumber, @Nullable Bundle queryArgs)842         static PhonebookArgs createFromEfName(Uri uri, int subscriptionId,
843                 String efName, int recordNumber, @Nullable Bundle queryArgs) {
844             int efType;
845             int efid;
846             if (efName != null) {
847                 switch (efName) {
848                     case ElementaryFiles.PATH_SEGMENT_EF_ADN:
849                         efType = ElementaryFiles.EF_ADN;
850                         efid = IccConstants.EF_ADN;
851                         break;
852                     case ElementaryFiles.PATH_SEGMENT_EF_FDN:
853                         efType = ElementaryFiles.EF_FDN;
854                         efid = IccConstants.EF_FDN;
855                         break;
856                     case ElementaryFiles.PATH_SEGMENT_EF_SDN:
857                         efType = ElementaryFiles.EF_SDN;
858                         efid = IccConstants.EF_SDN;
859                         break;
860                     default:
861                         throw new IllegalArgumentException(
862                                 "Unrecognized elementary file " + efName);
863                 }
864             } else {
865                 efType = ElementaryFiles.EF_UNKNOWN;
866                 efid = 0;
867             }
868             return new PhonebookArgs(uri, subscriptionId, efName, efType, efid, recordNumber,
869                     queryArgs);
870         }
871 
872         /**
873          * Pattern: elementary_files/subid/${subscriptionId}/${efName}
874          *
875          * e.g. elementary_files/subid/1/adn
876          *
877          * @see ElementaryFiles#getItemUri(int, int)
878          * @see #ELEMENTARY_FILES_ITEM
879          */
forElementaryFilesItem(Uri uri)880         static PhonebookArgs forElementaryFilesItem(Uri uri) {
881             int subscriptionId = parseSubscriptionIdFromUri(uri, 2);
882             String efName = uri.getPathSegments().get(3);
883             return PhonebookArgs.createFromEfName(
884                     uri, subscriptionId, efName, -1, null);
885         }
886 
887         /**
888          * Pattern: subid/${subscriptionId}/${efName}
889          *
890          * <p>e.g. subid/1/adn
891          *
892          * @see SimRecords#getContentUri(int, int)
893          * @see #SIM_RECORDS
894          */
forSimRecords(Uri uri, Bundle queryArgs)895         static PhonebookArgs forSimRecords(Uri uri, Bundle queryArgs) {
896             int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
897             String efName = uri.getPathSegments().get(2);
898             return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, -1, queryArgs);
899         }
900 
901         /**
902          * Pattern: subid/${subscriptionId}/${efName}/${recordNumber}
903          *
904          * <p>e.g. subid/1/adn/10
905          *
906          * @see SimRecords#getItemUri(int, int, int)
907          * @see #SIM_RECORDS_ITEM
908          */
forSimRecordsItem(Uri uri, Bundle queryArgs)909         static PhonebookArgs forSimRecordsItem(Uri uri, Bundle queryArgs) {
910             int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
911             String efName = uri.getPathSegments().get(2);
912             int recordNumber = parseRecordNumberFromUri(uri, 3);
913             return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, recordNumber,
914                     queryArgs);
915         }
916 
parseSubscriptionIdFromUri(Uri uri, int pathIndex)917         private static int parseSubscriptionIdFromUri(Uri uri, int pathIndex) {
918             if (pathIndex == -1) {
919                 return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
920             }
921             String segment = uri.getPathSegments().get(pathIndex);
922             try {
923                 return Integer.parseInt(segment);
924             } catch (NumberFormatException e) {
925                 throw new IllegalArgumentException("Invalid subscription ID: " + segment);
926             }
927         }
928 
parseRecordNumberFromUri(Uri uri, int pathIndex)929         private static int parseRecordNumberFromUri(Uri uri, int pathIndex) {
930             try {
931                 return Integer.parseInt(uri.getPathSegments().get(pathIndex));
932             } catch (NumberFormatException e) {
933                 throw new IllegalArgumentException(
934                         "Invalid record index: " + uri.getLastPathSegment());
935             }
936         }
937     }
938 }
939