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