blob: 8307672fcdc1bbda53e09e989eb76641a42d3f9e [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
19import android.Manifest;
20import android.annotation.TestApi;
21import android.content.ContentProvider;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.UriMatcher;
25import android.content.pm.PackageManager;
26import android.database.ContentObserver;
27import android.database.Cursor;
28import android.database.MatrixCursor;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.CancellationSignal;
32import android.os.RemoteException;
33import android.provider.SimPhonebookContract;
34import android.provider.SimPhonebookContract.ElementaryFiles;
35import android.provider.SimPhonebookContract.SimRecords;
36import android.telephony.PhoneNumberUtils;
37import android.telephony.Rlog;
38import android.telephony.SubscriptionInfo;
39import android.telephony.SubscriptionManager;
40import android.telephony.TelephonyFrameworkInitializer;
41import android.telephony.TelephonyManager;
42import android.util.ArraySet;
43import android.util.Pair;
44
45import androidx.annotation.NonNull;
46import androidx.annotation.Nullable;
47
48import com.android.internal.annotations.VisibleForTesting;
49import com.android.internal.telephony.IIccPhoneBook;
50import com.android.internal.telephony.uicc.AdnRecord;
51import com.android.internal.telephony.uicc.IccConstants;
52
53import com.google.common.base.Joiner;
54import com.google.common.base.Strings;
55import com.google.common.collect.ImmutableSet;
56import com.google.common.util.concurrent.MoreExecutors;
57
58import java.util.ArrayList;
59import java.util.Arrays;
60import java.util.LinkedHashSet;
61import java.util.List;
62import java.util.Objects;
63import java.util.Set;
64import java.util.concurrent.TimeUnit;
65import java.util.concurrent.locks.Lock;
66import java.util.concurrent.locks.ReentrantLock;
67import java.util.function.Supplier;
68
69/**
70 * Provider for contact records stored on the SIM card.
71 *
72 * @see SimPhonebookContract
73 */
74public class SimPhonebookProvider extends ContentProvider {
75
76 @VisibleForTesting
77 static final String[] ELEMENTARY_FILES_ALL_COLUMNS = {
78 ElementaryFiles.SLOT_INDEX,
79 ElementaryFiles.SUBSCRIPTION_ID,
80 ElementaryFiles.EF_TYPE,
81 ElementaryFiles.MAX_RECORDS,
82 ElementaryFiles.RECORD_COUNT,
83 ElementaryFiles.NAME_MAX_LENGTH,
84 ElementaryFiles.PHONE_NUMBER_MAX_LENGTH
85 };
86 @VisibleForTesting
87 static final String[] SIM_RECORDS_ALL_COLUMNS = {
88 SimRecords.SUBSCRIPTION_ID,
89 SimRecords.ELEMENTARY_FILE_TYPE,
90 SimRecords.RECORD_NUMBER,
91 SimRecords.NAME,
92 SimRecords.PHONE_NUMBER
93 };
94 private static final String TAG = "SimPhonebookProvider";
95 private static final Set<String> ELEMENTARY_FILES_COLUMNS_SET =
96 ImmutableSet.copyOf(ELEMENTARY_FILES_ALL_COLUMNS);
97 private static final Set<String> SIM_RECORDS_WRITABLE_COLUMNS = ImmutableSet.of(
98 SimRecords.NAME, SimRecords.PHONE_NUMBER
99 );
100
101 private static final int WRITE_TIMEOUT_SECONDS = 30;
102
103 private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
104
105 private static final int ELEMENTARY_FILES = 100;
106 private static final int ELEMENTARY_FILES_ITEM = 101;
107 private static final int SIM_RECORDS = 200;
108 private static final int SIM_RECORDS_ITEM = 201;
109 private static final int VALIDATE_NAME = 300;
110
111 static {
112 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
113 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES);
114 URI_MATCHER.addURI(
115 SimPhonebookContract.AUTHORITY,
116 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/"
117 + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*",
118 ELEMENTARY_FILES_ITEM);
119 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
120 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS);
121 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
122 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM);
123 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY,
124 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/"
125 + SimRecords.VALIDATE_NAME_PATH_SEGMENT,
126 VALIDATE_NAME);
127 }
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
212
213 @Nullable
214 @Override
215 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs,
216 @Nullable CancellationSignal cancellationSignal) {
217 if (queryArgs != null && (queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION)
218 || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS)
219 || queryArgs.containsKey(ContentResolver.QUERY_ARG_SQL_LIMIT))) {
220 throw new IllegalArgumentException(
221 "A SQL selection was provided but it is not supported by this provider.");
222 }
223 switch (URI_MATCHER.match(uri)) {
224 case ELEMENTARY_FILES:
225 return queryElementaryFiles(projection);
226 case ELEMENTARY_FILES_ITEM:
227 return queryElementaryFilesItem(PhonebookArgs.forElementaryFilesItem(uri),
228 projection);
229 case SIM_RECORDS:
230 return querySimRecords(PhonebookArgs.forSimRecords(uri, queryArgs), projection);
231 case SIM_RECORDS_ITEM:
232 return querySimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, queryArgs),
233 projection);
234 case VALIDATE_NAME:
235 return queryValidateName(PhonebookArgs.forValidateName(uri, queryArgs), queryArgs);
236 default:
237 throw new IllegalArgumentException("Unsupported Uri " + uri);
238 }
239 }
240
241 @Nullable
242 @Override
243 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
244 @Nullable String[] selectionArgs, @Nullable String sortOrder,
245 @Nullable CancellationSignal cancellationSignal) {
246 throw new UnsupportedOperationException("Only query with Bundle is supported");
247 }
248
249 @Nullable
250 @Override
251 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
252 @Nullable String[] selectionArgs, @Nullable String sortOrder) {
253 throw new UnsupportedOperationException("Only query with Bundle is supported");
254 }
255
256 private Cursor queryElementaryFiles(String[] projection) {
257 validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
258 if (projection == null) {
259 projection = ELEMENTARY_FILES_ALL_COLUMNS;
260 }
261
262 MatrixCursor result = new MatrixCursor(projection);
263
264 List<SubscriptionInfo> activeSubscriptions = getActiveSubscriptionInfoList();
265 for (SubscriptionInfo subInfo : activeSubscriptions) {
266 try {
267 addEfToCursor(result, subInfo, ElementaryFiles.EF_ADN);
268 addEfToCursor(result, subInfo, ElementaryFiles.EF_FDN);
269 addEfToCursor(result, subInfo, ElementaryFiles.EF_SDN);
270 } catch (RemoteException e) {
271 // Return an empty cursor. If service to access it is throwing remote
272 // exceptions then it's basically the same as not having a SIM.
273 return new MatrixCursor(projection, 0);
274 }
275 }
276 return result;
277 }
278
279 private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) {
280 validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
281
282 MatrixCursor result = new MatrixCursor(projection);
283 try {
284 addEfToCursor(
285 result, getActiveSubscriptionInfo(args.subscriptionId), args.efType);
286 } catch (RemoteException e) {
287 // Return an empty cursor. If service to access it is throwing remote
288 // exceptions then it's basically the same as not having a SIM.
289 return new MatrixCursor(projection, 0);
290 }
291 return result;
292 }
293
294 private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
295 int efType) throws RemoteException {
296 int[] recordsSize = mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
297 subscriptionInfo.getSubscriptionId(), efIdForEfType(efType));
298 addEfToCursor(result, subscriptionInfo, efType, recordsSize);
299 }
300
301 private void addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo,
302 int efType, int[] recordsSize) throws RemoteException {
303 // If the record count is zero then the SIM doesn't support the elementary file so just
304 // omit it.
305 if (recordsSize == null || getRecordCount(recordsSize) == 0) {
306 return;
307 }
308 MatrixCursor.RowBuilder row = result.newRow()
309 .add(ElementaryFiles.SLOT_INDEX, subscriptionInfo.getSimSlotIndex())
310 .add(ElementaryFiles.SUBSCRIPTION_ID, subscriptionInfo.getSubscriptionId())
311 .add(ElementaryFiles.EF_TYPE, efType)
312 .add(ElementaryFiles.MAX_RECORDS, getRecordCount(recordsSize))
313 .add(ElementaryFiles.NAME_MAX_LENGTH,
314 AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)))
315 .add(ElementaryFiles.PHONE_NUMBER_MAX_LENGTH,
316 AdnRecord.getMaxPhoneNumberDigits());
317 if (result.getColumnIndex(ElementaryFiles.RECORD_COUNT) != -1) {
318 int efid = efIdForEfType(efType);
319 List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get()
320 .getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid);
321 int nonEmptyCount = 0;
322 for (AdnRecord record : existingRecords) {
323 if (!record.isEmpty()) {
324 nonEmptyCount++;
325 }
326 }
327 row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount);
328 }
329 }
330
331 private Cursor querySimRecords(PhonebookArgs args, String[] projection) {
332 validateSubscriptionAndEf(args);
333 if (projection == null) {
334 projection = SIM_RECORDS_ALL_COLUMNS;
335 }
336
337 List<AdnRecord> records = loadRecordsForEf(args);
338 if (records == null) {
339 return new MatrixCursor(projection, 0);
340 }
341 MatrixCursor result = new MatrixCursor(projection, records.size());
342 List<Pair<AdnRecord, MatrixCursor.RowBuilder>> rowBuilders = new ArrayList<>(
343 records.size());
344 for (AdnRecord record : records) {
345 if (!record.isEmpty()) {
346 rowBuilders.add(Pair.create(record, result.newRow()));
347 }
348 }
349 // This is kind of ugly but avoids looking up columns in an inner loop.
350 for (String column : projection) {
351 switch (column) {
352 case SimRecords.SUBSCRIPTION_ID:
353 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
354 row.second.add(args.subscriptionId);
355 }
356 break;
357 case SimRecords.ELEMENTARY_FILE_TYPE:
358 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
359 row.second.add(args.efType);
360 }
361 break;
362 case SimRecords.RECORD_NUMBER:
363 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
364 row.second.add(row.first.getRecId());
365 }
366 break;
367 case SimRecords.NAME:
368 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
369 row.second.add(row.first.getAlphaTag());
370 }
371 break;
372 case SimRecords.PHONE_NUMBER:
373 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) {
374 row.second.add(row.first.getNumber());
375 }
376 break;
377 default:
378 Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri);
379 break;
380 }
381 }
382 return result;
383 }
384
385 private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) {
386 if (projection == null) {
387 projection = SIM_RECORDS_ALL_COLUMNS;
388 }
389 validateSubscriptionAndEf(args);
390 AdnRecord record = loadRecord(args);
391
392 MatrixCursor result = new MatrixCursor(projection, 1);
393 if (record == null || record.isEmpty()) {
394 return result;
395 }
396 result.newRow()
397 .add(SimRecords.SUBSCRIPTION_ID, args.subscriptionId)
398 .add(SimRecords.ELEMENTARY_FILE_TYPE, args.efType)
399 .add(SimRecords.RECORD_NUMBER, record.getRecId())
400 .add(SimRecords.NAME, record.getAlphaTag())
401 .add(SimRecords.PHONE_NUMBER, record.getNumber());
402 return result;
403 }
404
405 private Cursor queryValidateName(PhonebookArgs args, @Nullable Bundle queryArgs) {
406 if (queryArgs == null) {
407 throw new IllegalArgumentException(SimRecords.NAME + " is required.");
408 }
409 validateSubscriptionAndEf(args);
410 String name = queryArgs.getString(SimRecords.NAME);
411
412 // Cursor extras are used to return the result.
413 Cursor result = new MatrixCursor(new String[0], 0);
414 Bundle extras = new Bundle();
415 extras.putParcelable(SimRecords.EXTRA_NAME_VALIDATION_RESULT, validateName(args, name));
416 result.setExtras(extras);
417 return result;
418 }
419
420 @Nullable
421 @Override
422 public String getType(@NonNull Uri uri) {
423 switch (URI_MATCHER.match(uri)) {
424 case ELEMENTARY_FILES:
425 return ElementaryFiles.CONTENT_TYPE;
426 case ELEMENTARY_FILES_ITEM:
427 return ElementaryFiles.CONTENT_ITEM_TYPE;
428 case SIM_RECORDS:
429 return SimRecords.CONTENT_TYPE;
430 case SIM_RECORDS_ITEM:
431 return SimRecords.CONTENT_ITEM_TYPE;
432 default:
433 throw new IllegalArgumentException("Unsupported Uri " + uri);
434 }
435 }
436
437 @Nullable
438 @Override
439 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
440 return insert(uri, values, null);
441 }
442
443 @Nullable
444 @Override
445 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
446 switch (URI_MATCHER.match(uri)) {
447 case SIM_RECORDS:
448 return insertSimRecord(PhonebookArgs.forSimRecords(uri, extras), values);
449 case ELEMENTARY_FILES:
450 case ELEMENTARY_FILES_ITEM:
451 case SIM_RECORDS_ITEM:
452 throw new UnsupportedOperationException(uri + " does not support insert");
453 default:
454 throw new IllegalArgumentException("Unsupported Uri " + uri);
455 }
456 }
457
458 private Uri insertSimRecord(PhonebookArgs args, ContentValues values) {
459 validateWritableEf(args, "insert");
460 validateSubscriptionAndEf(args);
461
462 if (values == null || values.isEmpty()) {
463 return null;
464 }
465 validateValues(args, values);
466 String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
467 String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
468
469 acquireWriteLockOrThrow();
470 try {
471 List<AdnRecord> records = loadRecordsForEf(args);
472 if (records == null) {
473 Rlog.e(TAG, "Failed to load existing records for " + args.uri);
474 return null;
475 }
476 AdnRecord emptyRecord = null;
477 for (AdnRecord record : records) {
478 if (record.isEmpty()) {
479 emptyRecord = record;
480 break;
481 }
482 }
483 if (emptyRecord == null) {
484 // When there are no empty records that means the EF is full.
485 throw new IllegalStateException(
486 args.uri + " is full. Please delete records to add new ones.");
487 }
488 boolean success = updateRecord(args, emptyRecord, args.pin2, newName, newPhoneNumber);
489 if (!success) {
490 Rlog.e(TAG, "Insert failed for " + args.uri);
491 // Something didn't work but since we don't have any more specific
492 // information to provide to the caller it's better to just return null
493 // rather than throwing and possibly crashing their process.
494 return null;
495 }
496 notifyChange();
497 return SimRecords.getItemUri(args.subscriptionId, args.efType, emptyRecord.getRecId());
498 } finally {
499 releaseWriteLock();
500 }
501 }
502
503 @Override
504 public int delete(@NonNull Uri uri, @Nullable String selection,
505 @Nullable String[] selectionArgs) {
506 throw new UnsupportedOperationException("Only delete with Bundle is supported");
507 }
508
509 @Override
510 public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
511 switch (URI_MATCHER.match(uri)) {
512 case SIM_RECORDS_ITEM:
513 return deleteSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras));
514 case ELEMENTARY_FILES:
515 case ELEMENTARY_FILES_ITEM:
516 case SIM_RECORDS:
517 throw new UnsupportedOperationException(uri + " does not support delete");
518 default:
519 throw new IllegalArgumentException("Unsupported Uri " + uri);
520 }
521 }
522
523 private int deleteSimRecordsItem(PhonebookArgs args) {
524 validateWritableEf(args, "delete");
525 validateSubscriptionAndEf(args);
526
527 acquireWriteLockOrThrow();
528 try {
529 AdnRecord record = loadRecord(args);
530 if (record == null || record.isEmpty()) {
531 return 0;
532 }
533 if (!updateRecord(args, record, args.pin2, "", "")) {
534 Rlog.e(TAG, "Failed to delete " + args.uri);
535 }
536 notifyChange();
537 } finally {
538 releaseWriteLock();
539 }
540 return 1;
541 }
542
543
544 @Override
545 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) {
546 switch (URI_MATCHER.match(uri)) {
547 case SIM_RECORDS_ITEM:
548 return updateSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras), values);
549 case ELEMENTARY_FILES:
550 case ELEMENTARY_FILES_ITEM:
551 case SIM_RECORDS:
552 throw new UnsupportedOperationException(uri + " does not support update");
553 default:
554 throw new IllegalArgumentException("Unsupported Uri " + uri);
555 }
556 }
557
558 @Override
559 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
560 @Nullable String[] selectionArgs) {
561 throw new UnsupportedOperationException("Only Update with bundle is supported");
562 }
563
564 private int updateSimRecordsItem(PhonebookArgs args, ContentValues values) {
565 validateWritableEf(args, "update");
566 validateSubscriptionAndEf(args);
567
568 if (values == null || values.isEmpty()) {
569 return 0;
570 }
571 validateValues(args, values);
572 String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME));
573 String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER));
574
575 acquireWriteLockOrThrow();
576
577 try {
578 AdnRecord record = loadRecord(args);
579
580 // Note we allow empty records to be updated. This is a bit weird because they are
581 // not returned by query methods but this allows a client application assign a name
582 // to a specific record number. This may be desirable in some phone app use cases since
583 // the record number is often used as a quick dial index.
584 if (record == null) {
585 return 0;
586 }
587 if (!updateRecord(args, record, args.pin2, newName, newPhoneNumber)) {
588 Rlog.e(TAG, "Failed to update " + args.uri);
589 return 0;
590 }
591 notifyChange();
592 } finally {
593 releaseWriteLock();
594 }
595 return 1;
596 }
597
598 void validateSubscriptionAndEf(PhonebookArgs args) {
599 SubscriptionInfo info =
600 args.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
601 ? getActiveSubscriptionInfo(args.subscriptionId)
602 : null;
603 if (info == null) {
604 throw new IllegalArgumentException("No active SIM with subscription ID "
605 + args.subscriptionId);
606 }
607
608 int[] recordsSize = getRecordsSizeForEf(args);
609 if (recordsSize == null || recordsSize[1] == 0) {
610 throw new IllegalArgumentException(args.efName
611 + " is not supported for SIM with subscription ID " + args.subscriptionId);
612 }
613 }
614
615 private void acquireWriteLockOrThrow() {
616 try {
617 if (!mWriteLock.tryLock(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
618 throw new IllegalStateException("Timeout waiting to write");
619 }
620 } catch (InterruptedException e) {
621 throw new IllegalStateException("Write failed");
622 }
623 }
624
625 private void releaseWriteLock() {
626 mWriteLock.unlock();
627 }
628
629 private void validateWritableEf(PhonebookArgs args, String operationName) {
630 if (args.efType == ElementaryFiles.EF_FDN) {
631 if (hasPermissionsForFdnWrite(args)) {
632 return;
633 }
634 }
635 if (args.efType != ElementaryFiles.EF_ADN) {
636 throw new UnsupportedOperationException(
637 args.uri + " does not support " + operationName);
638 }
639 }
640
641 private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
642 TelephonyManager telephonyManager = getContext().getSystemService(
643 TelephonyManager.class);
644 String callingPackage = getCallingPackage();
645 int granted = PackageManager.PERMISSION_DENIED;
646 if (callingPackage != null) {
647 granted = getContext().getPackageManager().checkPermission(
648 Manifest.permission.MODIFY_PHONE_STATE, callingPackage);
649 }
650 return granted == PackageManager.PERMISSION_GRANTED
651 || telephonyManager.hasCarrierPrivileges(args.subscriptionId);
652
653 }
654
655
656 private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2,
657 String newName, String newPhone) {
658 try {
659 return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber(
660 args.subscriptionId, existingRecord.getEfid(), newName, newPhone,
661 existingRecord.getRecId(),
662 pin2);
663 } catch (RemoteException e) {
664 return false;
665 }
666 }
667
668 private SimRecords.NameValidationResult validateName(
669 PhonebookArgs args, @Nullable String name) {
670 name = Strings.nullToEmpty(name);
671 int recordSize = getRecordSize(getRecordsSizeForEf(args));
672 // Validating the name consists of encoding the record in the binary format that it is
673 // stored on the SIM then decoding it and checking whether the decoded name is the same.
674 // The AOSP implementation of AdnRecord replaces unsupported characters with spaces during
675 // encoding.
676 // TODO: It would be good to update AdnRecord to support UCS-2 on the encode path (it
677 // supports it on the decode path). Right now it's not supported and so any non-latin
678 // characters will not be valid (at least in the AOSP implementation).
679 byte[] encodedName = AdnRecord.encodeAlphaTag(name);
680 String sanitizedName = AdnRecord.decodeAlphaTag(encodedName, 0, encodedName.length);
681 return new SimRecords.NameValidationResult(name, sanitizedName,
682 encodedName.length, AdnRecord.getMaxAlphaTagBytes(recordSize));
683 }
684
685 private void validatePhoneNumber(@Nullable String phoneNumber) {
686 if (phoneNumber == null || phoneNumber.isEmpty()) {
687 throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is required.");
688 }
689 int actualLength = phoneNumber.length();
690 // When encoded the "+" prefix sets a bit and so doesn't count against the maximum length
691 if (phoneNumber.startsWith("+")) {
692 actualLength--;
693 }
694 if (actualLength > AdnRecord.getMaxPhoneNumberDigits()) {
695 throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is too long.");
696 }
697 for (int i = 0; i < phoneNumber.length(); i++) {
698 char c = phoneNumber.charAt(i);
699 if (!PhoneNumberUtils.isNonSeparator(c)) {
700 throw new IllegalArgumentException(
701 SimRecords.PHONE_NUMBER + " contains unsupported characters.");
702 }
703 }
704 }
705
706 private void validateValues(PhonebookArgs args, ContentValues values) {
707 if (!SIM_RECORDS_WRITABLE_COLUMNS.containsAll(values.keySet())) {
708 Set<String> unsupportedColumns = new ArraySet<>(values.keySet());
709 unsupportedColumns.removeAll(SIM_RECORDS_WRITABLE_COLUMNS);
710 throw new IllegalArgumentException("Unsupported columns: " + Joiner.on(',')
711 .join(unsupportedColumns));
712 }
713
714 String phoneNumber = values.getAsString(SimRecords.PHONE_NUMBER);
715 validatePhoneNumber(phoneNumber);
716
717 String name = values.getAsString(SimRecords.NAME);
718 SimRecords.NameValidationResult result = validateName(args, name);
719
720 if (result.getEncodedLength() > result.getMaxEncodedLength()) {
721 throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
722 } else if (!Objects.equals(result.getName(), result.getSanitizedName())) {
723 throw new IllegalArgumentException(
724 SimRecords.NAME + " contains unsupported characters.");
725 }
726 }
727
728 private List<SubscriptionInfo> getActiveSubscriptionInfoList() {
729 // Getting the SubscriptionInfo requires READ_PHONE_STATE but we're only returning
730 // the subscription ID and slot index which are not sensitive information.
731 CallingIdentity identity = clearCallingIdentity();
732 try {
733 return mSubscriptionManager.getActiveSubscriptionInfoList();
734 } finally {
735 restoreCallingIdentity(identity);
736 }
737 }
738
739 private SubscriptionInfo getActiveSubscriptionInfo(int subId) {
740 // Getting the SubscriptionInfo requires READ_PHONE_STATE.
741 CallingIdentity identity = clearCallingIdentity();
742 try {
743 return mSubscriptionManager.getActiveSubscriptionInfo(subId);
744 } finally {
745 restoreCallingIdentity(identity);
746 }
747 }
748
749 private List<AdnRecord> loadRecordsForEf(PhonebookArgs args) {
750 try {
751 return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber(
752 args.subscriptionId, args.efid);
753 } catch (RemoteException e) {
754 return null;
755 }
756 }
757
758 private AdnRecord loadRecord(PhonebookArgs args) {
759 List<AdnRecord> records = loadRecordsForEf(args);
760 if (args.recordNumber > records.size()) {
761 return null;
762 }
763 AdnRecord result = records.get(args.recordNumber - 1);
764 // This should be true but the service could have a different implementation.
765 if (result.getRecId() == args.recordNumber) {
766 return result;
767 }
768 for (AdnRecord record : records) {
769 if (record.getRecId() == args.recordNumber) {
770 return result;
771 }
772 }
773 return null;
774 }
775
776
777 private int[] getRecordsSizeForEf(PhonebookArgs args) {
778 try {
779 return mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber(
780 args.subscriptionId, args.efid);
781 } catch (RemoteException e) {
782 return null;
783 }
784 }
785
786 void notifyChange() {
787 mContentNotifier.notifyChange(SimPhonebookContract.AUTHORITY_URI);
788 }
789
790 /** Testable wrapper around {@link ContentResolver#notifyChange(Uri, ContentObserver)} */
791 @TestApi
792 interface ContentNotifier {
793 void notifyChange(Uri uri);
794 }
795
796 /**
797 * Holds the arguments extracted from the Uri and query args for accessing the referenced
798 * phonebook data on a SIM.
799 */
800 private static class PhonebookArgs {
801 public final Uri uri;
802 public final int subscriptionId;
803 public final String efName;
804 public final int efType;
805 public final int efid;
806 public final int recordNumber;
807 public final String pin2;
808
809 PhonebookArgs(Uri uri, int subscriptionId, String efName,
810 @ElementaryFiles.EfType int efType, int efid, int recordNumber,
811 @Nullable Bundle queryArgs) {
812 this.uri = uri;
813 this.subscriptionId = subscriptionId;
814 this.efName = efName;
815 this.efType = efType;
816 this.efid = efid;
817 this.recordNumber = recordNumber;
818 pin2 = efType == ElementaryFiles.EF_FDN && queryArgs != null
819 ? queryArgs.getString(SimRecords.QUERY_ARG_PIN2)
820 : null;
821 }
822
823 static PhonebookArgs createFromEfName(Uri uri, int subscriptionId,
824 String efName, int recordNumber, @Nullable Bundle queryArgs) {
825 int efType;
826 int efid;
827 if (efName != null) {
828 switch (efName) {
829 case ElementaryFiles.EF_ADN_PATH_SEGMENT:
830 efType = ElementaryFiles.EF_ADN;
831 efid = IccConstants.EF_ADN;
832 break;
833 case ElementaryFiles.EF_FDN_PATH_SEGMENT:
834 efType = ElementaryFiles.EF_FDN;
835 efid = IccConstants.EF_FDN;
836 break;
837 case ElementaryFiles.EF_SDN_PATH_SEGMENT:
838 efType = ElementaryFiles.EF_SDN;
839 efid = IccConstants.EF_SDN;
840 break;
841 default:
842 throw new IllegalArgumentException(
843 "Unrecognized elementary file " + efName);
844 }
845 } else {
846 efType = ElementaryFiles.EF_UNKNOWN;
847 efid = 0;
848 }
849 return new PhonebookArgs(uri, subscriptionId, efName, efType, efid, recordNumber,
850 queryArgs);
851 }
852
853 /**
854 * Pattern: elementary_files/subid/${subscriptionId}/${efName}
855 *
856 * e.g. elementary_files/subid/1/adn
857 *
858 * @see ElementaryFiles#getItemUri(int, int)
859 * @see #ELEMENTARY_FILES_ITEM
860 */
861 static PhonebookArgs forElementaryFilesItem(Uri uri) {
862 int subscriptionId = parseSubscriptionIdFromUri(uri, 2);
863 String efName = uri.getPathSegments().get(3);
864 return PhonebookArgs.createFromEfName(
865 uri, subscriptionId, efName, -1, null);
866 }
867
868 /**
869 * Pattern: subid/${subscriptionId}/${efName}
870 *
871 * <p>e.g. subid/1/adn
872 *
873 * @see SimRecords#getContentUri(int, int)
874 * @see #SIM_RECORDS
875 */
876 static PhonebookArgs forSimRecords(Uri uri, Bundle queryArgs) {
877 int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
878 String efName = uri.getPathSegments().get(2);
879 return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, -1, queryArgs);
880 }
881
882 /**
883 * Pattern: subid/${subscriptionId}/${efName}/${recordNumber}
884 *
885 * <p>e.g. subid/1/adn/10
886 *
887 * @see SimRecords#getItemUri(int, int, int)
888 * @see #SIM_RECORDS_ITEM
889 */
890 static PhonebookArgs forSimRecordsItem(Uri uri, Bundle queryArgs) {
891 int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
892 String efName = uri.getPathSegments().get(2);
893 int recordNumber = parseRecordNumberFromUri(uri, 3);
894 return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, recordNumber,
895 queryArgs);
896 }
897
898 /**
899 * Pattern: subid/${subscriptionId}/${efName}/validate_name
900 *
901 * @see SimRecords#validateName(ContentResolver, int, int, String)
902 * @see #VALIDATE_NAME
903 */
904 static PhonebookArgs forValidateName(Uri uri, Bundle queryArgs) {
905 int subscriptionId = parseSubscriptionIdFromUri(uri, 1);
906 String efName = uri.getPathSegments().get(2);
907 return PhonebookArgs.createFromEfName(
908 uri, subscriptionId, efName, -1, queryArgs);
909 }
910
911 private static int parseSubscriptionIdFromUri(Uri uri, int pathIndex) {
912 if (pathIndex == -1) {
913 return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
914 }
915 String segment = uri.getPathSegments().get(pathIndex);
916 try {
917 return Integer.parseInt(segment);
918 } catch (NumberFormatException e) {
919 throw new IllegalArgumentException("Invalid subscription ID: " + segment);
920 }
921 }
922
923 private static int parseRecordNumberFromUri(Uri uri, int pathIndex) {
924 try {
925 return Integer.parseInt(uri.getPathSegments().get(pathIndex));
926 } catch (NumberFormatException e) {
927 throw new IllegalArgumentException(
928 "Invalid record index: " + uri.getLastPathSegment());
929 }
930 }
931 }
932}