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