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