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