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