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