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