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