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