blob: 895286526956e7942bce7b8f63d1a914efd25d09 [file] [log] [blame]
Marcus Hagerottb3769272020-10-30 14:27:33 -07001/*
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
17package com.android.phone;
18
Mengjun Lenge1085452021-04-09 14:11:22 +080019import static com.android.internal.telephony.IccProvider.STR_NEW_NUMBER;
Marcus Hagerott8151e2b2021-07-28 09:40:11 -070020import static com.android.internal.telephony.IccProvider.STR_NEW_TAG;
Mengjun Lenge1085452021-04-09 14:11:22 +080021
Marcus Hagerottb3769272020-10-30 14:27:33 -070022import android.Manifest;
23import android.annotation.TestApi;
24import android.content.ContentProvider;
25import android.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.UriMatcher;
28import android.content.pm.PackageManager;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.database.MatrixCursor;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.CancellationSignal;
35import android.os.RemoteException;
36import android.provider.SimPhonebookContract;
37import android.provider.SimPhonebookContract.ElementaryFiles;
38import android.provider.SimPhonebookContract.SimRecords;
39import android.telephony.PhoneNumberUtils;
40import android.telephony.Rlog;
41import android.telephony.SubscriptionInfo;
42import android.telephony.SubscriptionManager;
43import android.telephony.TelephonyFrameworkInitializer;
44import android.telephony.TelephonyManager;
45import android.util.ArraySet;
Marcus Hagerott073f8e22021-09-30 15:13:53 -070046import android.util.SparseArray;
Marcus Hagerottb3769272020-10-30 14:27:33 -070047
48import androidx.annotation.NonNull;
49import androidx.annotation.Nullable;
50
51import com.android.internal.annotations.VisibleForTesting;
52import com.android.internal.telephony.IIccPhoneBook;
53import com.android.internal.telephony.uicc.AdnRecord;
54import com.android.internal.telephony.uicc.IccConstants;
55
56import com.google.common.base.Joiner;
57import com.google.common.base.Strings;
Marcus Hagerott073f8e22021-09-30 15:13:53 -070058import com.google.common.collect.ImmutableList;
Marcus Hagerottb3769272020-10-30 14:27:33 -070059import com.google.common.collect.ImmutableSet;
60import com.google.common.util.concurrent.MoreExecutors;
61
Marcus Hagerottb3769272020-10-30 14:27:33 -070062import java.util.Arrays;
63import java.util.LinkedHashSet;
64import java.util.List;
Marcus Hagerott45599da2021-02-17 11:12:25 -080065import java.util.Objects;
Marcus Hagerottb3769272020-10-30 14:27:33 -070066import java.util.Set;
67import java.util.concurrent.TimeUnit;
68import java.util.concurrent.locks.Lock;
69import java.util.concurrent.locks.ReentrantLock;
70import java.util.function.Supplier;
71
72/**
73 * Provider for contact records stored on the SIM card.
74 *
75 * @see SimPhonebookContract
76 */
77public 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);
Marcus Hagerott88bedac2021-08-19 15:40:35 -0700100 private static final Set<String> SIM_RECORDS_COLUMNS_SET =
101 ImmutableSet.copyOf(SIM_RECORDS_ALL_COLUMNS);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700102 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;
Marcus Hagerottb3769272020-10-30 14:27:33 -0700114
115 static {
116 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
117 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES);
118 URI_MATCHER.addURI(
119 SimPhonebookContract.AUTHORITY,
120 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/"
121 + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*",
122 ELEMENTARY_FILES_ITEM);
123 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
124 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS);
125 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
126 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700127 }
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
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
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
159 private static int getRecordSize(int[] recordsSize) {
160 return recordsSize[0];
161 }
162
163 private static int getRecordCount(int[] recordsSize) {
164 return recordsSize[2];
165 }
166
167 /** Returns the IccPhoneBook used to load the AdnRecords. */
168 private static IIccPhoneBook getIccPhoneBook() {
169 return IIccPhoneBook.Stub.asInterface(TelephonyFrameworkInitializer
170 .getTelephonyServiceManager().getIccPhoneBookServiceRegisterer().get());
171 }
172
173 @Override
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
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
Marcus Hagerottf3b47612021-01-29 09:25:37 -0800212 @Nullable
213 @Override
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
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
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 }
Marcus Hagerottb3769272020-10-30 14:27:33 -0700237
238 @Nullable
239 @Override
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);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700259 default:
260 throw new IllegalArgumentException("Unsupported Uri " + uri);
261 }
262 }
263
264 @Nullable
265 @Override
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
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
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
302 private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) {
303 validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
Marcus Hagerott45599da2021-02-17 11:12:25 -0800304 if (projection == null) {
305 projection = ELEMENTARY_FILES_ALL_COLUMNS;
306 }
Marcus Hagerottb3769272020-10-30 14:27:33 -0700307
308 MatrixCursor result = new MatrixCursor(projection);
309 try {
Marcus Hagerott8151e2b2021-07-28 09:40:11 -0700310 SubscriptionInfo info = getActiveSubscriptionInfo(args.subscriptionId);
311 if (info != null) {
312 addEfToCursor(result, info, args.efType);
313 }
Marcus Hagerottb3769272020-10-30 14:27:33 -0700314 } 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
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
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 }
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700336 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 }
Marcus Hagerottb3769272020-10-30 14:27:33 -0700345 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)
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700349 .add(ElementaryFiles.MAX_RECORDS, existingRecords.size())
Marcus Hagerottb3769272020-10-30 14:27:33 -0700350 .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) {
Marcus Hagerottb3769272020-10-30 14:27:33 -0700355 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
365 private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
Marcus Hagerott88bedac2021-08-19 15:40:35 -0700366 validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700367 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());
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700377 SparseArray<MatrixCursor.RowBuilder> rowBuilders = new SparseArray<>(records.size());
378 for (int i = 0; i < records.size(); i++) {
379 AdnRecord record = records.get(i);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700380 if (!record.isEmpty()) {
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700381 rowBuilders.put(i, result.newRow());
Marcus Hagerottb3769272020-10-30 14:27:33 -0700382 }
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:
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700388 for (int i = 0; i < rowBuilders.size(); i++) {
389 rowBuilders.valueAt(i).add(args.subscriptionId);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700390 }
391 break;
392 case SimRecords.ELEMENTARY_FILE_TYPE:
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700393 for (int i = 0; i < rowBuilders.size(); i++) {
394 rowBuilders.valueAt(i).add(args.efType);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700395 }
396 break;
397 case SimRecords.RECORD_NUMBER:
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700398 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);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700405 }
406 break;
407 case SimRecords.NAME:
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700408 for (int i = 0; i < rowBuilders.size(); i++) {
409 AdnRecord record = records.get(rowBuilders.keyAt(i));
410 rowBuilders.valueAt(i).add(record.getAlphaTag());
Marcus Hagerottb3769272020-10-30 14:27:33 -0700411 }
412 break;
413 case SimRecords.PHONE_NUMBER:
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700414 for (int i = 0; i < rowBuilders.size(); i++) {
415 AdnRecord record = records.get(rowBuilders.keyAt(i));
416 rowBuilders.valueAt(i).add(record.getNumber());
Marcus Hagerottb3769272020-10-30 14:27:33 -0700417 }
418 break;
419 default:
420 Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
421 break;
422 }
423 }
424 return result;
425 }
426
427 private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
Marcus Hagerott88bedac2021-08-19 15:40:35 -0700428 validateProjection(SIM_RECORDS_COLUMNS_SET, projection);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700429 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
Marcus Hagerottb3769272020-10-30 14:27:33 -0700448 @Nullable
449 @Override
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
467 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
468 return insert(uri, values, null);
469 }
470
471 @Nullable
472 @Override
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
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
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
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
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
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
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
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
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
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
653 private void releaseWriteLock() {
654 mWriteLock.unlock();
655 }
656
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
669 private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
Marcus Hagerott45599da2021-02-17 11:12:25 -0800670 TelephonyManager telephonyManager = Objects.requireNonNull(
671 getContext().getSystemService(TelephonyManager.class));
Marcus Hagerottb3769272020-10-30 14:27:33 -0700672 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
684 private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2,
685 String newName, String newPhone) {
686 try {
Mengjun Lenge1085452021-04-09 14:11:22 +0800687 ContentValues values = new ContentValues();
688 values.put(STR_NEW_TAG, newName);
689 values.put(STR_NEW_NUMBER, newPhone);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700690 return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber(
Mengjun Lenge1085452021-04-09 14:11:22 +0800691 args.subscriptionId, existingRecord.getEfid(), values,
Marcus Hagerottb3769272020-10-30 14:27:33 -0700692 existingRecord.getRecId(),
693 pin2);
694 } catch (RemoteException e) {
695 return false;
696 }
697 }
698
Marcus Hagerottb3769272020-10-30 14:27:33 -0700699 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
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);
Marcus Hagerottf3b47612021-01-29 09:25:37 -0800732 int length = getEncodedNameLength(name);
Marcus Hagerott45599da2021-02-17 11:12:25 -0800733 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));
Marcus Hagerottb3769272020-10-30 14:27:33 -0700739
Marcus Hagerottf3b47612021-01-29 09:25:37 -0800740 if (length > maxLength) {
Marcus Hagerottb3769272020-10-30 14:27:33 -0700741 throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
Marcus Hagerottb3769272020-10-30 14:27:33 -0700742 }
743 }
744
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
Marcus Hagerott8151e2b2021-07-28 09:40:11 -0700756 @Nullable
Marcus Hagerottb3769272020-10-30 14:27:33 -0700757 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
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
776 private AdnRecord loadRecord(PhonebookArgs args) {
777 List<AdnRecord> records = loadRecordsForEf(args);
Marcus Hagerott45599da2021-02-17 11:12:25 -0800778 if (records == null || args.recordNumber > records.size()) {
Marcus Hagerottb3769272020-10-30 14:27:33 -0700779 return null;
780 }
Marcus Hagerott073f8e22021-09-30 15:13:53 -0700781 return records.get(args.recordNumber - 1);
Marcus Hagerottb3769272020-10-30 14:27:33 -0700782 }
783
Marcus Hagerottb3769272020-10-30 14:27:33 -0700784 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
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 {
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
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
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) {
Marcus Hagerott1b79cd52021-03-22 13:37:50 -0700836 case ElementaryFiles.PATH_SEGMENT_EF_ADN:
Marcus Hagerottb3769272020-10-30 14:27:33 -0700837 efType = ElementaryFiles.EF_ADN;
838 efid = IccConstants.EF_ADN;
839 break;
Marcus Hagerott1b79cd52021-03-22 13:37:50 -0700840 case ElementaryFiles.PATH_SEGMENT_EF_FDN:
Marcus Hagerottb3769272020-10-30 14:27:33 -0700841 efType = ElementaryFiles.EF_FDN;
842 efid = IccConstants.EF_FDN;
843 break;
Marcus Hagerott1b79cd52021-03-22 13:37:50 -0700844 case ElementaryFiles.PATH_SEGMENT_EF_SDN:
Marcus Hagerottb3769272020-10-30 14:27:33 -0700845 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 */
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 */
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 */
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
Marcus Hagerottb3769272020-10-30 14:27:33 -0700905 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
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}