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