1 /* 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 17 package com.android.phone; 18 19 import static com.android.internal.telephony.IccProvider.STR_NEW_NUMBER; 20 import static com.android.internal.telephony.IccProvider.STR_NEW_TAG; 21 22 import android.Manifest; 23 import android.annotation.TestApi; 24 import android.content.ContentProvider; 25 import android.content.ContentResolver; 26 import android.content.ContentValues; 27 import android.content.UriMatcher; 28 import android.content.pm.PackageManager; 29 import android.database.ContentObserver; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.net.Uri; 33 import android.os.Binder; 34 import android.os.Bundle; 35 import android.os.CancellationSignal; 36 import android.os.RemoteException; 37 import android.provider.SimPhonebookContract; 38 import android.provider.SimPhonebookContract.ElementaryFiles; 39 import android.provider.SimPhonebookContract.SimRecords; 40 import android.telephony.PhoneNumberUtils; 41 import android.telephony.Rlog; 42 import android.telephony.SubscriptionInfo; 43 import android.telephony.SubscriptionManager; 44 import android.telephony.TelephonyFrameworkInitializer; 45 import android.telephony.TelephonyManager; 46 import android.util.ArraySet; 47 import android.util.SparseArray; 48 49 import androidx.annotation.NonNull; 50 import androidx.annotation.Nullable; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.internal.telephony.IIccPhoneBook; 54 import com.android.internal.telephony.flags.Flags; 55 import com.android.internal.telephony.uicc.AdnRecord; 56 import com.android.internal.telephony.uicc.IccConstants; 57 58 import com.google.common.base.Joiner; 59 import com.google.common.base.Strings; 60 import com.google.common.collect.ImmutableList; 61 import com.google.common.collect.ImmutableSet; 62 import com.google.common.util.concurrent.MoreExecutors; 63 64 import java.util.Arrays; 65 import java.util.LinkedHashSet; 66 import java.util.List; 67 import java.util.Objects; 68 import java.util.Set; 69 import java.util.concurrent.TimeUnit; 70 import java.util.concurrent.locks.Lock; 71 import java.util.concurrent.locks.ReentrantLock; 72 import java.util.function.Supplier; 73 74 /** 75 * Provider for contact records stored on the SIM card. 76 * 77 * @see SimPhonebookContract 78 */ 79 public 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); 102 private static final Set<String> SIM_RECORDS_COLUMNS_SET = 103 ImmutableSet.copyOf(SIM_RECORDS_ALL_COLUMNS); 104 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; 116 117 static { URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES)118 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 119 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES); URI_MATCHER.addURI( SimPhonebookContract.AUTHORITY, ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/" + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", ELEMENTARY_FILES_ITEM)120 URI_MATCHER.addURI( 121 SimPhonebookContract.AUTHORITY, 122 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/" 123 + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", 124 ELEMENTARY_FILES_ITEM); URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS)125 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 126 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS); URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM)127 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 128 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM); 129 } 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 efIdForEfType(@lementaryFiles.EfType int efType)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 validateProjection(Set<String> allowed, String[] projection)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 getRecordSize(int[] recordsSize)161 private static int getRecordSize(int[] recordsSize) { 162 return recordsSize[0]; 163 } 164 getRecordCount(int[] recordsSize)165 private static int getRecordCount(int[] recordsSize) { 166 return recordsSize[2]; 167 } 168 169 /** Returns the IccPhoneBook used to load the AdnRecords. */ getIccPhoneBook()170 private static IIccPhoneBook getIccPhoneBook() { 171 return IIccPhoneBook.Stub.asInterface(TelephonyFrameworkInitializer 172 .getTelephonyServiceManager().getIccPhoneBookServiceRegisterer().get()); 173 } 174 175 @Override onCreate()176 public boolean onCreate() { 177 ContentResolver resolver = getContext().getContentResolver(); 178 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, 186 SimPhonebookProvider::getIccPhoneBook, 187 uri -> resolver.notifyChange(uri, null)); 188 } 189 190 @TestApi onCreate(@onNull SubscriptionManager subscriptionManager, Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier)191 boolean onCreate(@NonNull SubscriptionManager subscriptionManager, 192 Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier) { 193 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 218 @Nullable 219 @Override call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)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 callForEncodedNameLength(String name)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 getEncodedNameLength(String name)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 } 243 244 @Nullable 245 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)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); 265 default: 266 throw new IllegalArgumentException("Unsupported Uri " + uri); 267 } 268 } 269 270 @Nullable 271 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)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 query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)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 queryElementaryFiles(String[] projection)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 queryElementaryFilesItem(PhonebookArgs args, String[] projection)308 private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) { 309 validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection); 310 if (projection == null) { 311 projection = ELEMENTARY_FILES_ALL_COLUMNS; 312 } 313 314 MatrixCursor result = new MatrixCursor(projection); 315 try { 316 SubscriptionInfo info = getActiveSubscriptionInfo(args.subscriptionId); 317 if (info != null) { 318 addEfToCursor(result, info, args.efType); 319 } 320 } 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 addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType)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 addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType, int[] recordsSize)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 } 342 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 } 351 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) 355 .add(ElementaryFiles.MAX_RECORDS, existingRecords.size()) 356 .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) { 361 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 querySimRecords(PhonebookArgs args, String[] projection)371 private Cursor querySimRecords(PhonebookArgs args, String[] projection) { 372 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 373 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()); 383 SparseArray<MatrixCursor.RowBuilder> rowBuilders = new SparseArray<>(records.size()); 384 for (int i = 0; i < records.size(); i++) { 385 AdnRecord record = records.get(i); 386 if (!record.isEmpty()) { 387 rowBuilders.put(i, result.newRow()); 388 } 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: 394 for (int i = 0; i < rowBuilders.size(); i++) { 395 rowBuilders.valueAt(i).add(args.subscriptionId); 396 } 397 break; 398 case SimRecords.ELEMENTARY_FILE_TYPE: 399 for (int i = 0; i < rowBuilders.size(); i++) { 400 rowBuilders.valueAt(i).add(args.efType); 401 } 402 break; 403 case SimRecords.RECORD_NUMBER: 404 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); 411 } 412 break; 413 case SimRecords.NAME: 414 for (int i = 0; i < rowBuilders.size(); i++) { 415 AdnRecord record = records.get(rowBuilders.keyAt(i)); 416 rowBuilders.valueAt(i).add(record.getAlphaTag()); 417 } 418 break; 419 case SimRecords.PHONE_NUMBER: 420 for (int i = 0; i < rowBuilders.size(); i++) { 421 AdnRecord record = records.get(rowBuilders.keyAt(i)); 422 rowBuilders.valueAt(i).add(record.getNumber()); 423 } 424 break; 425 default: 426 Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri); 427 break; 428 } 429 } 430 return result; 431 } 432 querySimRecordsItem(PhonebookArgs args, String[] projection)433 private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) { 434 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 435 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 454 @Nullable 455 @Override getType(@onNull Uri uri)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 insert(@onNull Uri uri, @Nullable ContentValues values)473 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 474 return insert(uri, values, null); 475 } 476 477 @Nullable 478 @Override insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)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 insertSimRecord(PhonebookArgs args, ContentValues values)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 delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)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 delete(@onNull Uri uri, @Nullable Bundle extras)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 deleteSimRecordsItem(PhonebookArgs args)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 update(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)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 update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)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 updateSimRecordsItem(PhonebookArgs args, ContentValues values)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 validateSubscriptionAndEf(PhonebookArgs args)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 acquireWriteLockOrThrow()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 releaseWriteLock()659 private void releaseWriteLock() { 660 mWriteLock.unlock(); 661 } 662 validateWritableEf(PhonebookArgs args, String operationName)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 hasPermissionsForFdnWrite(PhonebookArgs args)675 private boolean hasPermissionsForFdnWrite(PhonebookArgs args) { 676 TelephonyManager telephonyManager = Objects.requireNonNull( 677 getContext().getSystemService(TelephonyManager.class)); 678 String callingPackage = getCallingPackage(); 679 int granted = PackageManager.PERMISSION_DENIED; 680 if (callingPackage != null) { 681 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 } 689 } 690 return granted == PackageManager.PERMISSION_GRANTED 691 || telephonyManager.hasCarrierPrivileges(args.subscriptionId); 692 693 } 694 695 updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, String newName, String newPhone)696 private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, 697 String newName, String newPhone) { 698 try { 699 ContentValues values = new ContentValues(); 700 values.put(STR_NEW_TAG, newName); 701 values.put(STR_NEW_NUMBER, newPhone); 702 return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber( 703 args.subscriptionId, existingRecord.getEfid(), values, 704 existingRecord.getRecId(), 705 pin2); 706 } catch (RemoteException e) { 707 return false; 708 } 709 } 710 validatePhoneNumber(@ullable String phoneNumber)711 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 validateValues(PhonebookArgs args, ContentValues values)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); 744 int length = getEncodedNameLength(name); 745 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)); 751 752 if (length > maxLength) { 753 throw new IllegalArgumentException(SimRecords.NAME + " is too long."); 754 } 755 } 756 getActiveSubscriptionInfoList()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 768 @Nullable getActiveSubscriptionInfo(int subId)769 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 loadRecordsForEf(PhonebookArgs args)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 loadRecord(PhonebookArgs args)788 private AdnRecord loadRecord(PhonebookArgs args) { 789 List<AdnRecord> records = loadRecordsForEf(args); 790 if (records == null || args.recordNumber > records.size()) { 791 return null; 792 } 793 return records.get(args.recordNumber - 1); 794 } 795 getRecordsSizeForEf(PhonebookArgs args)796 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 notifyChange()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 { notifyChange(Uri uri)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 PhonebookArgs(Uri uri, int subscriptionId, String efName, @ElementaryFiles.EfType int efType, int efid, int recordNumber, @Nullable Bundle queryArgs)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 createFromEfName(Uri uri, int subscriptionId, String efName, int recordNumber, @Nullable Bundle queryArgs)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) { 848 case ElementaryFiles.PATH_SEGMENT_EF_ADN: 849 efType = ElementaryFiles.EF_ADN; 850 efid = IccConstants.EF_ADN; 851 break; 852 case ElementaryFiles.PATH_SEGMENT_EF_FDN: 853 efType = ElementaryFiles.EF_FDN; 854 efid = IccConstants.EF_FDN; 855 break; 856 case ElementaryFiles.PATH_SEGMENT_EF_SDN: 857 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 */ forElementaryFilesItem(Uri uri)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 */ forSimRecords(Uri uri, Bundle queryArgs)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 */ forSimRecordsItem(Uri uri, Bundle queryArgs)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 parseSubscriptionIdFromUri(Uri uri, int pathIndex)917 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 parseRecordNumberFromUri(Uri uri, int pathIndex)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 } 939