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.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_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 + R + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + R, 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 + R, 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 + R, 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         MatrixCursor.RowBuilder row = result.newRow()
337                 .add(ElementaryFiles.SLOT_INDEX, subscriptionInfo.getSimSlotIndex())
338                 .add(ElementaryFiles.SUBSCRIPTION_ID, subscriptionInfo.getSubscriptionId())
339                 .add(ElementaryFiles.EF_TYPE, efType)
340                 .add(ElementaryFiles.MAX_RECORDS, getRecordCount(recordsSize))
341                 .add(ElementaryFiles.NAME_MAX_LENGTH,
342                         AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)))
343                 .add(ElementaryFiles.PHONE_NUMBER_MAX_LENGTH,
344                         AdnRecord.getMaxPhoneNumberDigits());
345         if (result.getColumnIndex(ElementaryFiles.RECORD_COUNT) != -1) {
346             int efid = efIdForEfType(efType);
347             List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get()
348                     .getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid);
349             int nonEmptyCount = 0;
350             for (AdnRecord record : existingRecords) {
351                 if (!record.isEmpty()) {
352                     nonEmptyCount++;
353                 }
354             }
355             row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount);
356         }
357     }
358 
querySimRecords(PhonebookArgs args, String[] projection)359     private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
360         validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
361         validateSubscriptionAndEf(args);
362         if (projection == null) {
363             projection = SIM_RECORDS_ALL_COLUMNS;
364         }
365 
366         List<AdnRecord> records = loadRecordsForEf(args);
367         if (records == null) {
368             return new MatrixCursor(projection, 0);
369         }
370         MatrixCursor result = new MatrixCursor(projection, records.size());
371         List<Pair<AdnRecord, MatrixCursor.RowBuilder>> rowBuilders = new ArrayList<>(
372                 records.size());
373         for (AdnRecord record : records) {
374             if (!record.isEmpty()) {
375                 rowBuilders.add(Pair.create(record, result.newRow()));
376             }
377         }
378         // This is kind of ugly but avoids looking up columns in an inner loop.
379         for (String column : projection) {
380             switch (column) {
381                 case SimRecords.SUBSCRIPTION_ID:
382                     for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
383                         row.second.add(args.subscriptionId);
384                     }
385                     break;
386                 case SimRecords.ELEMENTARY_FILE_TYPE:
387                     for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
388                         row.second.add(args.efType);
389                     }
390                     break;
391                 case SimRecords.RECORD_NUMBER:
392                     for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
393                         row.second.add(row.first.getRecId());
394                     }
395                     break;
396                 case SimRecords.NAME:
397                     for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
398                         row.second.add(row.first.getAlphaTag());
399                     }
400                     break;
401                 case SimRecords.PHONE_NUMBER:
402                     for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
403                         row.second.add(row.first.getNumber());
404                     }
405                     break;
406                 default:
407                     Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
408                     break;
409             }
410         }
411         return result;
412     }
413 
querySimRecordsItem(PhonebookArgs args, String[] projection)414     private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
415         validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
416         if (projection == null) {
417             projection = SIM_RECORDS_ALL_COLUMNS;
418         }
419         validateSubscriptionAndEf(args);
420         AdnRecord record = loadRecord(args);
421 
422         MatrixCursor result = new MatrixCursor(projection, 1);
423         if (record == null || record.isEmpty()) {
424             return result;
425         }
426         result.newRow()
427                 .add(SimRecords.SUBSCRIPTION_ID, args.subscriptionId)
428                 .add(SimRecords.ELEMENTARY_FILE_TYPE, args.efType)
429                 .add(SimRecords.RECORD_NUMBER, record.getRecId())
430                 .add(SimRecords.NAME, record.getAlphaTag())
431                 .add(SimRecords.PHONE_NUMBER, record.getNumber());
432         return result;
433     }
434 
435     @Nullable
436     @Override
getType(@onNull Uri uri)437     public String getType(@NonNull Uri uri) {
438         switch (URI_MATCHER.match(uri)) {
439             case ELEMENTARY_FILES:
440                 return ElementaryFiles.CONTENT_TYPE;
441             case ELEMENTARY_FILES_ITEM:
442                 return ElementaryFiles.CONTENT_ITEM_TYPE;
443             case SIM_RECORDS:
444                 return SimRecords.CONTENT_TYPE;
445             case SIM_RECORDS_ITEM:
446                 return SimRecords.CONTENT_ITEM_TYPE;
447             default:
448                 throw new IllegalArgumentException("Unsupported Uri " + uri);
449         }
450     }
451 
452     @Nullable
453     @Override
insert(@onNull Uri uri, @Nullable ContentValues values)454     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
455         return insert(uri, values, null);
456     }
457 
458     @Nullable
459     @Override
insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)460     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
461         switch (URI_MATCHER.match(uri)) {
462             case SIM_RECORDS:
463                 return insertSimRecord(PhonebookArgs.forSimRecords(uri, extras), values);
464             case ELEMENTARY_FILES:
465             case ELEMENTARY_FILES_ITEM:
466             case SIM_RECORDS_ITEM:
467                 throw new UnsupportedOperationException(uri + " does not support insert");
468             default:
469                 throw new IllegalArgumentException("Unsupported Uri " + uri);
470         }
471     }
472 
insertSimRecord(PhonebookArgs args, ContentValues values)473     private Uri insertSimRecord(PhonebookArgs args, ContentValues values) {
474         validateWritableEf(args, "insert");
475         validateSubscriptionAndEf(args);
476 
477         if (values == null || values.isEmpty()) {
478             return null;
479         }
480         validateValues(args, values);
481         String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
482         String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
483 
484         acquireWriteLockOrThrow();
485         try {
486             List<AdnRecord> records = loadRecordsForEf(args);
487             if (records == null) {
488                 Rlog.e(TAG, "Failed to load existing records for " + args.uri);
489                 return null;
490             }
491             AdnRecord emptyRecord = null;
492             for (AdnRecord record : records) {
493                 if (record.isEmpty()) {
494                     emptyRecord = record;
495                     break;
496                 }
497             }
498             if (emptyRecord == null) {
499                 // When there are no empty records that means the EF is full.
500                 throw new IllegalStateException(
501                         args.uri + " is full. Please delete records to add new ones.");
502             }
503             boolean success = updateRecord(args, emptyRecord, args.pin2, newName, newPhoneNumber);
504             if (!success) {
505                 Rlog.e(TAG, "Insert failed for " + args.uri);
506                 // Something didn't work but since we don't have any more specific
507                 // information to provide to the caller it's better to just return null
508                 // rather than throwing and possibly crashing their process.
509                 return null;
510             }
511             notifyChange();
512             return SimRecords.getItemUri(args.subscriptionId, args.efType, emptyRecord.getRecId());
513         } finally {
514             releaseWriteLock();
515         }
516     }
517 
518     @Override
delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)519     public int delete(@NonNull Uri uri, @Nullable String selection,
520             @Nullable String[] selectionArgs) {
521         throw new UnsupportedOperationException("Only delete with Bundle is supported");
522     }
523 
524     @Override
delete(@onNull Uri uri, @Nullable Bundle extras)525     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
526         switch (URI_MATCHER.match(uri)) {
527             case SIM_RECORDS_ITEM:
528                 return deleteSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras));
529             case ELEMENTARY_FILES:
530             case ELEMENTARY_FILES_ITEM:
531             case SIM_RECORDS:
532                 throw new UnsupportedOperationException(uri + " does not support delete");
533             default:
534                 throw new IllegalArgumentException("Unsupported Uri " + uri);
535         }
536     }
537 
deleteSimRecordsItem(PhonebookArgs args)538     private int deleteSimRecordsItem(PhonebookArgs args) {
539         validateWritableEf(args, "delete");
540         validateSubscriptionAndEf(args);
541 
542         acquireWriteLockOrThrow();
543         try {
544             AdnRecord record = loadRecord(args);
545             if (record == null || record.isEmpty()) {
546                 return 0;
547             }
548             if (!updateRecord(args, record, args.pin2, "", "")) {
549                 Rlog.e(TAG, "Failed to delete " + args.uri);
550             }
551             notifyChange();
552         } finally {
553             releaseWriteLock();
554         }
555         return 1;
556     }
557 
558 
559     @Override
update(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)560     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
561         switch (URI_MATCHER.match(uri)) {
562             case SIM_RECORDS_ITEM:
563                 return updateSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras), values);
564             case ELEMENTARY_FILES:
565             case ELEMENTARY_FILES_ITEM:
566             case SIM_RECORDS:
567                 throw new UnsupportedOperationException(uri + " does not support update");
568             default:
569                 throw new IllegalArgumentException("Unsupported Uri " + uri);
570         }
571     }
572 
573     @Override
update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)574     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
575             @Nullable String[] selectionArgs) {
576         throw new UnsupportedOperationException("Only Update with bundle is supported");
577     }
578 
updateSimRecordsItem(PhonebookArgs args, ContentValues values)579     private int updateSimRecordsItem(PhonebookArgs args, ContentValues values) {
580         validateWritableEf(args, "update");
581         validateSubscriptionAndEf(args);
582 
583         if (values == null || values.isEmpty()) {
584             return 0;
585         }
586         validateValues(args, values);
587         String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
588         String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
589 
590         acquireWriteLockOrThrow();
591 
592         try {
593             AdnRecord record = loadRecord(args);
594 
595             // Note we allow empty records to be updated. This is a bit weird because they are
596             // not returned by query methods but this allows a client application assign a name
597             // to a specific record number. This may be desirable in some phone app use cases since
598             // the record number is often used as a quick dial index.
599             if (record == null) {
600                 return 0;
601             }
602             if (!updateRecord(args, record, args.pin2, newName, newPhoneNumber)) {
603                 Rlog.e(TAG, "Failed to update " + args.uri);
604                 return 0;
605             }
606             notifyChange();
607         } finally {
608             releaseWriteLock();
609         }
610         return 1;
611     }
612 
validateSubscriptionAndEf(PhonebookArgs args)613     void validateSubscriptionAndEf(PhonebookArgs args) {
614         SubscriptionInfo info =
615                 args.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
616                         ? getActiveSubscriptionInfo(args.subscriptionId)
617                         : null;
618         if (info == null) {
619             throw new IllegalArgumentException("No active SIM with subscription ID "
620                     + args.subscriptionId);
621         }
622 
623         int[] recordsSize = getRecordsSizeForEf(args);
624         if (recordsSize == null || recordsSize[1] == 0) {
625             throw new IllegalArgumentException(args.efName
626                     + " is not supported for SIM with subscription ID " + args.subscriptionId);
627         }
628     }
629 
acquireWriteLockOrThrow()630     private void acquireWriteLockOrThrow() {
631         try {
632             if (!mWriteLock.tryLock(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
633                 throw new IllegalStateException("Timeout waiting to write");
634             }
635         } catch (InterruptedException e) {
636             throw new IllegalStateException("Write failed");
637         }
638     }
639 
releaseWriteLock()640     private void releaseWriteLock() {
641         mWriteLock.unlock();
642     }
643 
validateWritableEf(PhonebookArgs args, String operationName)644     private void validateWritableEf(PhonebookArgs args, String operationName) {
645         if (args.efType == ElementaryFiles.EF_FDN) {
646             if (hasPermissionsForFdnWrite(args)) {
647                 return;
648             }
649         }
650         if (args.efType != ElementaryFiles.EF_ADN) {
651             throw new UnsupportedOperationException(
652                     args.uri + " does not support " + operationName);
653         }
654     }
655 
hasPermissionsForFdnWrite(PhonebookArgs args)656     private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
657         TelephonyManager telephonyManager = Objects.requireNonNull(
658                 getContext().getSystemService(TelephonyManager.class));
659         String callingPackage = getCallingPackage();
660         int granted = PackageManager.PERMISSION_DENIED;
661         if (callingPackage != null) {
662             granted = getContext().getPackageManager().checkPermission(
663                     Manifest.permission.MODIFY_PHONE_STATE, callingPackage);
664         }
665         return granted == PackageManager.PERMISSION_GRANTED
666                 || telephonyManager.hasCarrierPrivileges(args.subscriptionId);
667 
668     }
669 
670 
updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, String newName, String newPhone)671     private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2,
672             String newName, String newPhone) {
673         try {
674             ContentValues values = new ContentValues();
675             values.put(STR_NEW_TAG, newName);
676             values.put(STR_NEW_NUMBER, newPhone);
677             return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber(
678                     args.subscriptionId, existingRecord.getEfid(), values,
679                     existingRecord.getRecId(),
680                     pin2);
681         } catch (RemoteException e) {
682             return false;
683         }
684     }
685 
validatePhoneNumber(@ullable String phoneNumber)686     private void validatePhoneNumber(@Nullable String phoneNumber) {
687         if (phoneNumber == null || phoneNumber.isEmpty()) {
688             throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is required.");
689         }
690         int actualLength = phoneNumber.length();
691         // When encoded the "+" prefix sets a bit and so doesn't count against the maximum length
692         if (phoneNumber.startsWith("+")) {
693             actualLength--;
694         }
695         if (actualLength > AdnRecord.getMaxPhoneNumberDigits()) {
696             throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is too long.");
697         }
698         for (int i = 0; i < phoneNumber.length(); i++) {
699             char c = phoneNumber.charAt(i);
700             if (!PhoneNumberUtils.isNonSeparator(c)) {
701                 throw new IllegalArgumentException(
702                         SimRecords.PHONE_NUMBER + " contains unsupported characters.");
703             }
704         }
705     }
706 
validateValues(PhonebookArgs args, ContentValues values)707     private void validateValues(PhonebookArgs args, ContentValues values) {
708         if (!SIM_RECORDS_WRITABLE_COLUMNS.containsAll(values.keySet())) {
709             Set<String> unsupportedColumns = new ArraySet<>(values.keySet());
710             unsupportedColumns.removeAll(SIM_RECORDS_WRITABLE_COLUMNS);
711             throw new IllegalArgumentException("Unsupported columns: " + Joiner.on(',')
712                     .join(unsupportedColumns));
713         }
714 
715         String phoneNumber = values.getAsString(SimRecords.PHONE_NUMBER);
716         validatePhoneNumber(phoneNumber);
717 
718         String name = values.getAsString(SimRecords.NAME);
719         int length = getEncodedNameLength(name);
720         int[] recordsSize = getRecordsSizeForEf(args);
721         if (recordsSize == null) {
722             throw new IllegalStateException(
723                     "Failed to get " + ElementaryFiles.NAME_MAX_LENGTH + " from SIM");
724         }
725         int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize));
726 
727         if (length > maxLength) {
728             throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
729         }
730     }
731 
getActiveSubscriptionInfoList()732     private List<SubscriptionInfo> getActiveSubscriptionInfoList() {
733         // Getting the SubscriptionInfo requires READ_PHONE_STATE but we're only returning
734         // the subscription ID and slot index which are not sensitive information.
735         CallingIdentity identity = clearCallingIdentity();
736         try {
737             return mSubscriptionManager.getActiveSubscriptionInfoList();
738         } finally {
739             restoreCallingIdentity(identity);
740         }
741     }
742 
743     @Nullable
getActiveSubscriptionInfo(int subId)744     private SubscriptionInfo getActiveSubscriptionInfo(int subId) {
745         // Getting the SubscriptionInfo requires READ_PHONE_STATE.
746         CallingIdentity identity = clearCallingIdentity();
747         try {
748             return mSubscriptionManager.getActiveSubscriptionInfo(subId);
749         } finally {
750             restoreCallingIdentity(identity);
751         }
752     }
753 
loadRecordsForEf(PhonebookArgs args)754     private List<AdnRecord> loadRecordsForEf(PhonebookArgs args) {
755         try {
756             return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber(
757                     args.subscriptionId, args.efid);
758         } catch (RemoteException e) {
759             return null;
760         }
761     }
762 
loadRecord(PhonebookArgs args)763     private AdnRecord loadRecord(PhonebookArgs args) {
764         List<AdnRecord> records = loadRecordsForEf(args);
765         if (records == null || args.recordNumber > records.size()) {
766             return null;
767         }
768         AdnRecord result = records.get(args.recordNumber - 1);
769         // This should be true but the service could have a different implementation.
770         if (result.getRecId() == args.recordNumber) {
771             return result;
772         }
773         for (AdnRecord record : records) {
774             if (record.getRecId() == args.recordNumber) {
775                 return result;
776             }
777         }
778         return null;
779     }
780 
781 
getRecordsSizeForEf(PhonebookArgs args)782     private int[] getRecordsSizeForEf(PhonebookArgs args) {
783         try {
784             return mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
785                     args.subscriptionId, args.efid);
786         } catch (RemoteException e) {
787             return null;
788         }
789     }
790 
notifyChange()791     void notifyChange() {
792         mContentNotifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
793     }
794 
795     /** Testable wrapper around {@link ContentResolver#notifyChange(Uri, ContentObserver)} */
796     @TestApi
797     interface ContentNotifier {
notifyChange(Uri uri)798         void notifyChange(Uri uri);
799     }
800 
801     /**
802      * Holds the arguments extracted from the Uri and query args for accessing the referenced
803      * phonebook data on a SIM.
804      */
805     private static class PhonebookArgs {
806         public final Uri uri;
807         public final int subscriptionId;
808         public final String efName;
809         public final int efType;
810         public final int efid;
811         public final int recordNumber;
812         public final String pin2;
813 
PhonebookArgs(Uri uri, int subscriptionId, String efName, @ElementaryFiles.EfType int efType, int efid, int recordNumber, @Nullable Bundle queryArgs)814         PhonebookArgs(Uri uri, int subscriptionId, String efName,
815                 @ElementaryFiles.EfType int efType, int efid, int recordNumber,
816                 @Nullable Bundle queryArgs) {
817             this.uri = uri;
818             this.subscriptionId = subscriptionId;
819             this.efName = efName;
820             this.efType = efType;
821             this.efid = efid;
822             this.recordNumber = recordNumber;
823             pin2 = efType == ElementaryFiles.EF_FDN && queryArgs != null
824                     ? queryArgs.getString(SimRecords.QUERY_ARG_PIN2)
825                     : null;
826         }
827 
createFromEfName(Uri uri, int subscriptionId, String efName, int recordNumber, @Nullable Bundle queryArgs)828         static PhonebookArgs createFromEfName(Uri uri, int subscriptionId,
829                 String efName, int recordNumber, @Nullable Bundle queryArgs) {
830             int efType;
831             int efid;
832             if (efName != null) {
833                 switch (efName) {
834                     case ElementaryFiles.PATH_SEGMENT_EF_ADN:
835                         efType = ElementaryFiles.EF_ADN;
836                         efid = IccConstants.EF_ADN;
837                         break;
838                     case ElementaryFiles.PATH_SEGMENT_EF_FDN:
839                         efType = ElementaryFiles.EF_FDN;
840                         efid = IccConstants.EF_FDN;
841                         break;
842                     case ElementaryFiles.PATH_SEGMENT_EF_SDN:
843                         efType = ElementaryFiles.EF_SDN;
844                         efid = IccConstants.EF_SDN;
845                         break;
846                     default:
847                         throw new IllegalArgumentException(
848                                 "Unrecognized elementary file " + efName);
849                 }
850             } else {
851                 efType = ElementaryFiles.EF_UNKNOWN;
852                 efid = 0;
853             }
854             return new PhonebookArgs(uri, subscriptionId, efName, efType, efid, recordNumber,
855                     queryArgs);
856         }
857 
858         /**
859          * Pattern: elementary_files/subid/${subscriptionId}/${efName}
860          *
861          * e.g. elementary_files/subid/1/adn
862          *
863          * @see ElementaryFiles#getItemUri(int, int)
864          * @see #ELEMENTARY_FILES_ITEM
865          */
forElementaryFilesItem(Uri uri)866         static PhonebookArgs forElementaryFilesItem(Uri uri) {
867             int subscriptionId = parseSubscriptionIdFromUri(uri, 2);
868             String efName = uri.getPathSegments().get(3);
869             return PhonebookArgs.createFromEfName(
870                     uri, subscriptionId, efName, -1, null);
871         }
872 
873         /**
874          * Pattern: subid/${subscriptionId}/${efName}
875          *
876          * <p>e.g. subid/1/adn
877          *
878          * @see SimRecords#getContentUri(int, int)
879          * @see #SIM_RECORDS
880          */
forSimRecords(Uri uri, Bundle queryArgs)881         static PhonebookArgs forSimRecords(Uri uri, Bundle queryArgs) {
882             int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
883             String efName = uri.getPathSegments().get(2);
884             return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, -1, queryArgs);
885         }
886 
887         /**
888          * Pattern: subid/${subscriptionId}/${efName}/${recordNumber}
889          *
890          * <p>e.g. subid/1/adn/10
891          *
892          * @see SimRecords#getItemUri(int, int, int)
893          * @see #SIM_RECORDS_ITEM
894          */
forSimRecordsItem(Uri uri, Bundle queryArgs)895         static PhonebookArgs forSimRecordsItem(Uri uri, Bundle queryArgs) {
896             int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
897             String efName = uri.getPathSegments().get(2);
898             int recordNumber = parseRecordNumberFromUri(uri, 3);
899             return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, recordNumber,
900                     queryArgs);
901         }
902 
parseSubscriptionIdFromUri(Uri uri, int pathIndex)903         private static int parseSubscriptionIdFromUri(Uri uri, int pathIndex) {
904             if (pathIndex == -1) {
905                 return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
906             }
907             String segment = uri.getPathSegments().get(pathIndex);
908             try {
909                 return Integer.parseInt(segment);
910             } catch (NumberFormatException e) {
911                 throw new IllegalArgumentException("Invalid subscription ID: " + segment);
912             }
913         }
914 
parseRecordNumberFromUri(Uri uri, int pathIndex)915         private static int parseRecordNumberFromUri(Uri uri, int pathIndex) {
916             try {
917                 return Integer.parseInt(uri.getPathSegments().get(pathIndex));
918             } catch (NumberFormatException e) {
919                 throw new IllegalArgumentException(
920                         "Invalid record index: " + uri.getLastPathSegment());
921             }
922         }
923     }
924 }
925