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.Bundle; 34 import android.os.CancellationSignal; 35 import android.os.RemoteException; 36 import android.provider.SimPhonebookContract; 37 import android.provider.SimPhonebookContract.ElementaryFiles; 38 import android.provider.SimPhonebookContract.SimRecords; 39 import android.telephony.PhoneNumberUtils; 40 import android.telephony.Rlog; 41 import android.telephony.SubscriptionInfo; 42 import android.telephony.SubscriptionManager; 43 import android.telephony.TelephonyFrameworkInitializer; 44 import android.telephony.TelephonyManager; 45 import android.util.ArraySet; 46 import android.util.SparseArray; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.telephony.IIccPhoneBook; 53 import com.android.internal.telephony.flags.Flags; 54 import com.android.internal.telephony.uicc.AdnRecord; 55 import com.android.internal.telephony.uicc.IccConstants; 56 57 import com.google.common.base.Joiner; 58 import com.google.common.base.Strings; 59 import com.google.common.collect.ImmutableList; 60 import com.google.common.collect.ImmutableSet; 61 import com.google.common.util.concurrent.MoreExecutors; 62 63 import java.util.Arrays; 64 import java.util.LinkedHashSet; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.Set; 68 import java.util.concurrent.TimeUnit; 69 import java.util.concurrent.locks.Lock; 70 import java.util.concurrent.locks.ReentrantLock; 71 import java.util.function.Supplier; 72 73 /** 74 * Provider for contact records stored on the SIM card. 75 * 76 * @see SimPhonebookContract 77 */ 78 public 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); 101 private static final Set<String> SIM_RECORDS_COLUMNS_SET = 102 ImmutableSet.copyOf(SIM_RECORDS_ALL_COLUMNS); 103 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; 115 116 static { URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT, ELEMENTARY_FILES)117 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 118 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)119 URI_MATCHER.addURI( 120 SimPhonebookContract.AUTHORITY, 121 ElementaryFiles.ELEMENTARY_FILES_PATH_SEGMENT + "/" 122 + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", 123 ELEMENTARY_FILES_ITEM); URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS)124 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 125 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*", SIM_RECORDS); URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM)126 URI_MATCHER.addURI(SimPhonebookContract.AUTHORITY, 127 SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + "/#/*/#", SIM_RECORDS_ITEM); 128 } 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 efIdForEfType(@lementaryFiles.EfType int efType)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 validateProjection(Set<String> allowed, String[] projection)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 getRecordSize(int[] recordsSize)160 private static int getRecordSize(int[] recordsSize) { 161 return recordsSize[0]; 162 } 163 getRecordCount(int[] recordsSize)164 private static int getRecordCount(int[] recordsSize) { 165 return recordsSize[2]; 166 } 167 168 /** Returns the IccPhoneBook used to load the AdnRecords. */ getIccPhoneBook()169 private static IIccPhoneBook getIccPhoneBook() { 170 return IIccPhoneBook.Stub.asInterface(TelephonyFrameworkInitializer 171 .getTelephonyServiceManager().getIccPhoneBookServiceRegisterer().get()); 172 } 173 174 @Override onCreate()175 public boolean onCreate() { 176 ContentResolver resolver = getContext().getContentResolver(); 177 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, 185 SimPhonebookProvider::getIccPhoneBook, 186 uri -> resolver.notifyChange(uri, null)); 187 } 188 189 @TestApi onCreate(@onNull SubscriptionManager subscriptionManager, Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier)190 boolean onCreate(@NonNull SubscriptionManager subscriptionManager, 191 Supplier<IIccPhoneBook> iccPhoneBookSupplier, ContentNotifier notifier) { 192 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 217 @Nullable 218 @Override call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)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 callForEncodedNameLength(String name)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 getEncodedNameLength(String name)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 } 242 243 @Nullable 244 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)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); 264 default: 265 throw new IllegalArgumentException("Unsupported Uri " + uri); 266 } 267 } 268 269 @Nullable 270 @Override query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal)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 query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)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 queryElementaryFiles(String[] projection)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 queryElementaryFilesItem(PhonebookArgs args, String[] projection)307 private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) { 308 validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection); 309 if (projection == null) { 310 projection = ELEMENTARY_FILES_ALL_COLUMNS; 311 } 312 313 MatrixCursor result = new MatrixCursor(projection); 314 try { 315 SubscriptionInfo info = getActiveSubscriptionInfo(args.subscriptionId); 316 if (info != null) { 317 addEfToCursor(result, info, args.efType); 318 } 319 } 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 addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType)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 addEfToCursor(MatrixCursor result, SubscriptionInfo subscriptionInfo, int efType, int[] recordsSize)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 } 341 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 } 350 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) 354 .add(ElementaryFiles.MAX_RECORDS, existingRecords.size()) 355 .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) { 360 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 querySimRecords(PhonebookArgs args, String[] projection)370 private Cursor querySimRecords(PhonebookArgs args, String[] projection) { 371 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 372 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()); 382 SparseArray<MatrixCursor.RowBuilder> rowBuilders = new SparseArray<>(records.size()); 383 for (int i = 0; i < records.size(); i++) { 384 AdnRecord record = records.get(i); 385 if (!record.isEmpty()) { 386 rowBuilders.put(i, result.newRow()); 387 } 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: 393 for (int i = 0; i < rowBuilders.size(); i++) { 394 rowBuilders.valueAt(i).add(args.subscriptionId); 395 } 396 break; 397 case SimRecords.ELEMENTARY_FILE_TYPE: 398 for (int i = 0; i < rowBuilders.size(); i++) { 399 rowBuilders.valueAt(i).add(args.efType); 400 } 401 break; 402 case SimRecords.RECORD_NUMBER: 403 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); 410 } 411 break; 412 case SimRecords.NAME: 413 for (int i = 0; i < rowBuilders.size(); i++) { 414 AdnRecord record = records.get(rowBuilders.keyAt(i)); 415 rowBuilders.valueAt(i).add(record.getAlphaTag()); 416 } 417 break; 418 case SimRecords.PHONE_NUMBER: 419 for (int i = 0; i < rowBuilders.size(); i++) { 420 AdnRecord record = records.get(rowBuilders.keyAt(i)); 421 rowBuilders.valueAt(i).add(record.getNumber()); 422 } 423 break; 424 default: 425 Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri); 426 break; 427 } 428 } 429 return result; 430 } 431 querySimRecordsItem(PhonebookArgs args, String[] projection)432 private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) { 433 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 434 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 453 @Nullable 454 @Override getType(@onNull Uri uri)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 insert(@onNull Uri uri, @Nullable ContentValues values)472 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 473 return insert(uri, values, null); 474 } 475 476 @Nullable 477 @Override insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)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 insertSimRecord(PhonebookArgs args, ContentValues values)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 delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)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 delete(@onNull Uri uri, @Nullable Bundle extras)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 deleteSimRecordsItem(PhonebookArgs args)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 update(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)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 update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)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 updateSimRecordsItem(PhonebookArgs args, ContentValues values)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 validateSubscriptionAndEf(PhonebookArgs args)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 acquireWriteLockOrThrow()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 releaseWriteLock()658 private void releaseWriteLock() { 659 mWriteLock.unlock(); 660 } 661 validateWritableEf(PhonebookArgs args, String operationName)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 hasPermissionsForFdnWrite(PhonebookArgs args)674 private boolean hasPermissionsForFdnWrite(PhonebookArgs args) { 675 TelephonyManager telephonyManager = Objects.requireNonNull( 676 getContext().getSystemService(TelephonyManager.class)); 677 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 updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, String newName, String newPhone)689 private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, 690 String newName, String newPhone) { 691 try { 692 ContentValues values = new ContentValues(); 693 values.put(STR_NEW_TAG, newName); 694 values.put(STR_NEW_NUMBER, newPhone); 695 return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber( 696 args.subscriptionId, existingRecord.getEfid(), values, 697 existingRecord.getRecId(), 698 pin2); 699 } catch (RemoteException e) { 700 return false; 701 } 702 } 703 validatePhoneNumber(@ullable String phoneNumber)704 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 validateValues(PhonebookArgs args, ContentValues values)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); 737 int length = getEncodedNameLength(name); 738 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)); 744 745 if (length > maxLength) { 746 throw new IllegalArgumentException(SimRecords.NAME + " is too long."); 747 } 748 } 749 getActiveSubscriptionInfoList()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 761 @Nullable getActiveSubscriptionInfo(int subId)762 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 loadRecordsForEf(PhonebookArgs args)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 loadRecord(PhonebookArgs args)781 private AdnRecord loadRecord(PhonebookArgs args) { 782 List<AdnRecord> records = loadRecordsForEf(args); 783 if (records == null || args.recordNumber > records.size()) { 784 return null; 785 } 786 return records.get(args.recordNumber - 1); 787 } 788 getRecordsSizeForEf(PhonebookArgs args)789 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 notifyChange()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 { notifyChange(Uri uri)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 PhonebookArgs(Uri uri, int subscriptionId, String efName, @ElementaryFiles.EfType int efType, int efid, int recordNumber, @Nullable Bundle queryArgs)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 createFromEfName(Uri uri, int subscriptionId, String efName, int recordNumber, @Nullable Bundle queryArgs)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) { 841 case ElementaryFiles.PATH_SEGMENT_EF_ADN: 842 efType = ElementaryFiles.EF_ADN; 843 efid = IccConstants.EF_ADN; 844 break; 845 case ElementaryFiles.PATH_SEGMENT_EF_FDN: 846 efType = ElementaryFiles.EF_FDN; 847 efid = IccConstants.EF_FDN; 848 break; 849 case ElementaryFiles.PATH_SEGMENT_EF_SDN: 850 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 */ forElementaryFilesItem(Uri uri)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 */ forSimRecords(Uri uri, Bundle queryArgs)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 */ forSimRecordsItem(Uri uri, Bundle queryArgs)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 parseSubscriptionIdFromUri(Uri uri, int pathIndex)910 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 parseRecordNumberFromUri(Uri uri, int pathIndex)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 } 932