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