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