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.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_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 + R + SimPhonebookContract.SUBSCRIPTION_ID_PATH_SEGMENT + R, 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 + R, 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 + R, 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 MatrixCursor.RowBuilder row = result.newRow() 337 .add(ElementaryFiles.SLOT_INDEX, subscriptionInfo.getSimSlotIndex()) 338 .add(ElementaryFiles.SUBSCRIPTION_ID, subscriptionInfo.getSubscriptionId()) 339 .add(ElementaryFiles.EF_TYPE, efType) 340 .add(ElementaryFiles.MAX_RECORDS, getRecordCount(recordsSize)) 341 .add(ElementaryFiles.NAME_MAX_LENGTH, 342 AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize))) 343 .add(ElementaryFiles.PHONE_NUMBER_MAX_LENGTH, 344 AdnRecord.getMaxPhoneNumberDigits()); 345 if (result.getColumnIndex(ElementaryFiles.RECORD_COUNT) != -1) { 346 int efid = efIdForEfType(efType); 347 List<AdnRecord> existingRecords = mIccPhoneBookSupplier.get() 348 .getAdnRecordsInEfForSubscriber(subscriptionInfo.getSubscriptionId(), efid); 349 int nonEmptyCount = 0; 350 for (AdnRecord record : existingRecords) { 351 if (!record.isEmpty()) { 352 nonEmptyCount++; 353 } 354 } 355 row.add(ElementaryFiles.RECORD_COUNT, nonEmptyCount); 356 } 357 } 358 querySimRecords(PhonebookArgs args, String[] projection)359 private Cursor querySimRecords(PhonebookArgs args, String[] projection) { 360 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 361 validateSubscriptionAndEf(args); 362 if (projection == null) { 363 projection = SIM_RECORDS_ALL_COLUMNS; 364 } 365 366 List<AdnRecord> records = loadRecordsForEf(args); 367 if (records == null) { 368 return new MatrixCursor(projection, 0); 369 } 370 MatrixCursor result = new MatrixCursor(projection, records.size()); 371 List<Pair<AdnRecord, MatrixCursor.RowBuilder>> rowBuilders = new ArrayList<>( 372 records.size()); 373 for (AdnRecord record : records) { 374 if (!record.isEmpty()) { 375 rowBuilders.add(Pair.create(record, result.newRow())); 376 } 377 } 378 // This is kind of ugly but avoids looking up columns in an inner loop. 379 for (String column : projection) { 380 switch (column) { 381 case SimRecords.SUBSCRIPTION_ID: 382 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) { 383 row.second.add(args.subscriptionId); 384 } 385 break; 386 case SimRecords.ELEMENTARY_FILE_TYPE: 387 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) { 388 row.second.add(args.efType); 389 } 390 break; 391 case SimRecords.RECORD_NUMBER: 392 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) { 393 row.second.add(row.first.getRecId()); 394 } 395 break; 396 case SimRecords.NAME: 397 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) { 398 row.second.add(row.first.getAlphaTag()); 399 } 400 break; 401 case SimRecords.PHONE_NUMBER: 402 for (Pair<AdnRecord, MatrixCursor.RowBuilder> row : rowBuilders) { 403 row.second.add(row.first.getNumber()); 404 } 405 break; 406 default: 407 Rlog.w(TAG, "Column " + column + " is unsupported for " + args.uri); 408 break; 409 } 410 } 411 return result; 412 } 413 querySimRecordsItem(PhonebookArgs args, String[] projection)414 private Cursor querySimRecordsItem(PhonebookArgs args, String[] projection) { 415 validateProjection(SIM_RECORDS_COLUMNS_SET, projection); 416 if (projection == null) { 417 projection = SIM_RECORDS_ALL_COLUMNS; 418 } 419 validateSubscriptionAndEf(args); 420 AdnRecord record = loadRecord(args); 421 422 MatrixCursor result = new MatrixCursor(projection, 1); 423 if (record == null || record.isEmpty()) { 424 return result; 425 } 426 result.newRow() 427 .add(SimRecords.SUBSCRIPTION_ID, args.subscriptionId) 428 .add(SimRecords.ELEMENTARY_FILE_TYPE, args.efType) 429 .add(SimRecords.RECORD_NUMBER, record.getRecId()) 430 .add(SimRecords.NAME, record.getAlphaTag()) 431 .add(SimRecords.PHONE_NUMBER, record.getNumber()); 432 return result; 433 } 434 435 @Nullable 436 @Override getType(@onNull Uri uri)437 public String getType(@NonNull Uri uri) { 438 switch (URI_MATCHER.match(uri)) { 439 case ELEMENTARY_FILES: 440 return ElementaryFiles.CONTENT_TYPE; 441 case ELEMENTARY_FILES_ITEM: 442 return ElementaryFiles.CONTENT_ITEM_TYPE; 443 case SIM_RECORDS: 444 return SimRecords.CONTENT_TYPE; 445 case SIM_RECORDS_ITEM: 446 return SimRecords.CONTENT_ITEM_TYPE; 447 default: 448 throw new IllegalArgumentException("Unsupported Uri " + uri); 449 } 450 } 451 452 @Nullable 453 @Override insert(@onNull Uri uri, @Nullable ContentValues values)454 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 455 return insert(uri, values, null); 456 } 457 458 @Nullable 459 @Override insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)460 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) { 461 switch (URI_MATCHER.match(uri)) { 462 case SIM_RECORDS: 463 return insertSimRecord(PhonebookArgs.forSimRecords(uri, extras), values); 464 case ELEMENTARY_FILES: 465 case ELEMENTARY_FILES_ITEM: 466 case SIM_RECORDS_ITEM: 467 throw new UnsupportedOperationException(uri + " does not support insert"); 468 default: 469 throw new IllegalArgumentException("Unsupported Uri " + uri); 470 } 471 } 472 insertSimRecord(PhonebookArgs args, ContentValues values)473 private Uri insertSimRecord(PhonebookArgs args, ContentValues values) { 474 validateWritableEf(args, "insert"); 475 validateSubscriptionAndEf(args); 476 477 if (values == null || values.isEmpty()) { 478 return null; 479 } 480 validateValues(args, values); 481 String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME)); 482 String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER)); 483 484 acquireWriteLockOrThrow(); 485 try { 486 List<AdnRecord> records = loadRecordsForEf(args); 487 if (records == null) { 488 Rlog.e(TAG, "Failed to load existing records for " + args.uri); 489 return null; 490 } 491 AdnRecord emptyRecord = null; 492 for (AdnRecord record : records) { 493 if (record.isEmpty()) { 494 emptyRecord = record; 495 break; 496 } 497 } 498 if (emptyRecord == null) { 499 // When there are no empty records that means the EF is full. 500 throw new IllegalStateException( 501 args.uri + " is full. Please delete records to add new ones."); 502 } 503 boolean success = updateRecord(args, emptyRecord, args.pin2, newName, newPhoneNumber); 504 if (!success) { 505 Rlog.e(TAG, "Insert failed for " + args.uri); 506 // Something didn't work but since we don't have any more specific 507 // information to provide to the caller it's better to just return null 508 // rather than throwing and possibly crashing their process. 509 return null; 510 } 511 notifyChange(); 512 return SimRecords.getItemUri(args.subscriptionId, args.efType, emptyRecord.getRecId()); 513 } finally { 514 releaseWriteLock(); 515 } 516 } 517 518 @Override delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)519 public int delete(@NonNull Uri uri, @Nullable String selection, 520 @Nullable String[] selectionArgs) { 521 throw new UnsupportedOperationException("Only delete with Bundle is supported"); 522 } 523 524 @Override delete(@onNull Uri uri, @Nullable Bundle extras)525 public int delete(@NonNull Uri uri, @Nullable Bundle extras) { 526 switch (URI_MATCHER.match(uri)) { 527 case SIM_RECORDS_ITEM: 528 return deleteSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras)); 529 case ELEMENTARY_FILES: 530 case ELEMENTARY_FILES_ITEM: 531 case SIM_RECORDS: 532 throw new UnsupportedOperationException(uri + " does not support delete"); 533 default: 534 throw new IllegalArgumentException("Unsupported Uri " + uri); 535 } 536 } 537 deleteSimRecordsItem(PhonebookArgs args)538 private int deleteSimRecordsItem(PhonebookArgs args) { 539 validateWritableEf(args, "delete"); 540 validateSubscriptionAndEf(args); 541 542 acquireWriteLockOrThrow(); 543 try { 544 AdnRecord record = loadRecord(args); 545 if (record == null || record.isEmpty()) { 546 return 0; 547 } 548 if (!updateRecord(args, record, args.pin2, "", "")) { 549 Rlog.e(TAG, "Failed to delete " + args.uri); 550 } 551 notifyChange(); 552 } finally { 553 releaseWriteLock(); 554 } 555 return 1; 556 } 557 558 559 @Override update(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)560 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras) { 561 switch (URI_MATCHER.match(uri)) { 562 case SIM_RECORDS_ITEM: 563 return updateSimRecordsItem(PhonebookArgs.forSimRecordsItem(uri, extras), values); 564 case ELEMENTARY_FILES: 565 case ELEMENTARY_FILES_ITEM: 566 case SIM_RECORDS: 567 throw new UnsupportedOperationException(uri + " does not support update"); 568 default: 569 throw new IllegalArgumentException("Unsupported Uri " + uri); 570 } 571 } 572 573 @Override update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)574 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, 575 @Nullable String[] selectionArgs) { 576 throw new UnsupportedOperationException("Only Update with bundle is supported"); 577 } 578 updateSimRecordsItem(PhonebookArgs args, ContentValues values)579 private int updateSimRecordsItem(PhonebookArgs args, ContentValues values) { 580 validateWritableEf(args, "update"); 581 validateSubscriptionAndEf(args); 582 583 if (values == null || values.isEmpty()) { 584 return 0; 585 } 586 validateValues(args, values); 587 String newName = Strings.nullToEmpty(values.getAsString(SimRecords.NAME)); 588 String newPhoneNumber = Strings.nullToEmpty(values.getAsString(SimRecords.PHONE_NUMBER)); 589 590 acquireWriteLockOrThrow(); 591 592 try { 593 AdnRecord record = loadRecord(args); 594 595 // Note we allow empty records to be updated. This is a bit weird because they are 596 // not returned by query methods but this allows a client application assign a name 597 // to a specific record number. This may be desirable in some phone app use cases since 598 // the record number is often used as a quick dial index. 599 if (record == null) { 600 return 0; 601 } 602 if (!updateRecord(args, record, args.pin2, newName, newPhoneNumber)) { 603 Rlog.e(TAG, "Failed to update " + args.uri); 604 return 0; 605 } 606 notifyChange(); 607 } finally { 608 releaseWriteLock(); 609 } 610 return 1; 611 } 612 validateSubscriptionAndEf(PhonebookArgs args)613 void validateSubscriptionAndEf(PhonebookArgs args) { 614 SubscriptionInfo info = 615 args.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID 616 ? getActiveSubscriptionInfo(args.subscriptionId) 617 : null; 618 if (info == null) { 619 throw new IllegalArgumentException("No active SIM with subscription ID " 620 + args.subscriptionId); 621 } 622 623 int[] recordsSize = getRecordsSizeForEf(args); 624 if (recordsSize == null || recordsSize[1] == 0) { 625 throw new IllegalArgumentException(args.efName 626 + " is not supported for SIM with subscription ID " + args.subscriptionId); 627 } 628 } 629 acquireWriteLockOrThrow()630 private void acquireWriteLockOrThrow() { 631 try { 632 if (!mWriteLock.tryLock(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { 633 throw new IllegalStateException("Timeout waiting to write"); 634 } 635 } catch (InterruptedException e) { 636 throw new IllegalStateException("Write failed"); 637 } 638 } 639 releaseWriteLock()640 private void releaseWriteLock() { 641 mWriteLock.unlock(); 642 } 643 validateWritableEf(PhonebookArgs args, String operationName)644 private void validateWritableEf(PhonebookArgs args, String operationName) { 645 if (args.efType == ElementaryFiles.EF_FDN) { 646 if (hasPermissionsForFdnWrite(args)) { 647 return; 648 } 649 } 650 if (args.efType != ElementaryFiles.EF_ADN) { 651 throw new UnsupportedOperationException( 652 args.uri + " does not support " + operationName); 653 } 654 } 655 hasPermissionsForFdnWrite(PhonebookArgs args)656 private boolean hasPermissionsForFdnWrite(PhonebookArgs args) { 657 TelephonyManager telephonyManager = Objects.requireNonNull( 658 getContext().getSystemService(TelephonyManager.class)); 659 String callingPackage = getCallingPackage(); 660 int granted = PackageManager.PERMISSION_DENIED; 661 if (callingPackage != null) { 662 granted = getContext().getPackageManager().checkPermission( 663 Manifest.permission.MODIFY_PHONE_STATE, callingPackage); 664 } 665 return granted == PackageManager.PERMISSION_GRANTED 666 || telephonyManager.hasCarrierPrivileges(args.subscriptionId); 667 668 } 669 670 updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, String newName, String newPhone)671 private boolean updateRecord(PhonebookArgs args, AdnRecord existingRecord, String pin2, 672 String newName, String newPhone) { 673 try { 674 ContentValues values = new ContentValues(); 675 values.put(STR_NEW_TAG, newName); 676 values.put(STR_NEW_NUMBER, newPhone); 677 return mIccPhoneBookSupplier.get().updateAdnRecordsInEfByIndexForSubscriber( 678 args.subscriptionId, existingRecord.getEfid(), values, 679 existingRecord.getRecId(), 680 pin2); 681 } catch (RemoteException e) { 682 return false; 683 } 684 } 685 validatePhoneNumber(@ullable String phoneNumber)686 private void validatePhoneNumber(@Nullable String phoneNumber) { 687 if (phoneNumber == null || phoneNumber.isEmpty()) { 688 throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is required."); 689 } 690 int actualLength = phoneNumber.length(); 691 // When encoded the "+" prefix sets a bit and so doesn't count against the maximum length 692 if (phoneNumber.startsWith("+")) { 693 actualLength--; 694 } 695 if (actualLength > AdnRecord.getMaxPhoneNumberDigits()) { 696 throw new IllegalArgumentException(SimRecords.PHONE_NUMBER + " is too long."); 697 } 698 for (int i = 0; i < phoneNumber.length(); i++) { 699 char c = phoneNumber.charAt(i); 700 if (!PhoneNumberUtils.isNonSeparator(c)) { 701 throw new IllegalArgumentException( 702 SimRecords.PHONE_NUMBER + " contains unsupported characters."); 703 } 704 } 705 } 706 validateValues(PhonebookArgs args, ContentValues values)707 private void validateValues(PhonebookArgs args, ContentValues values) { 708 if (!SIM_RECORDS_WRITABLE_COLUMNS.containsAll(values.keySet())) { 709 Set<String> unsupportedColumns = new ArraySet<>(values.keySet()); 710 unsupportedColumns.removeAll(SIM_RECORDS_WRITABLE_COLUMNS); 711 throw new IllegalArgumentException("Unsupported columns: " + Joiner.on(',') 712 .join(unsupportedColumns)); 713 } 714 715 String phoneNumber = values.getAsString(SimRecords.PHONE_NUMBER); 716 validatePhoneNumber(phoneNumber); 717 718 String name = values.getAsString(SimRecords.NAME); 719 int length = getEncodedNameLength(name); 720 int[] recordsSize = getRecordsSizeForEf(args); 721 if (recordsSize == null) { 722 throw new IllegalStateException( 723 "Failed to get " + ElementaryFiles.NAME_MAX_LENGTH + " from SIM"); 724 } 725 int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize)); 726 727 if (length > maxLength) { 728 throw new IllegalArgumentException(SimRecords.NAME + " is too long."); 729 } 730 } 731 getActiveSubscriptionInfoList()732 private List<SubscriptionInfo> getActiveSubscriptionInfoList() { 733 // Getting the SubscriptionInfo requires READ_PHONE_STATE but we're only returning 734 // the subscription ID and slot index which are not sensitive information. 735 CallingIdentity identity = clearCallingIdentity(); 736 try { 737 return mSubscriptionManager.getActiveSubscriptionInfoList(); 738 } finally { 739 restoreCallingIdentity(identity); 740 } 741 } 742 743 @Nullable getActiveSubscriptionInfo(int subId)744 private SubscriptionInfo getActiveSubscriptionInfo(int subId) { 745 // Getting the SubscriptionInfo requires READ_PHONE_STATE. 746 CallingIdentity identity = clearCallingIdentity(); 747 try { 748 return mSubscriptionManager.getActiveSubscriptionInfo(subId); 749 } finally { 750 restoreCallingIdentity(identity); 751 } 752 } 753 loadRecordsForEf(PhonebookArgs args)754 private List<AdnRecord> loadRecordsForEf(PhonebookArgs args) { 755 try { 756 return mIccPhoneBookSupplier.get().getAdnRecordsInEfForSubscriber( 757 args.subscriptionId, args.efid); 758 } catch (RemoteException e) { 759 return null; 760 } 761 } 762 loadRecord(PhonebookArgs args)763 private AdnRecord loadRecord(PhonebookArgs args) { 764 List<AdnRecord> records = loadRecordsForEf(args); 765 if (records == null || args.recordNumber > records.size()) { 766 return null; 767 } 768 AdnRecord result = records.get(args.recordNumber - 1); 769 // This should be true but the service could have a different implementation. 770 if (result.getRecId() == args.recordNumber) { 771 return result; 772 } 773 for (AdnRecord record : records) { 774 if (record.getRecId() == args.recordNumber) { 775 return result; 776 } 777 } 778 return null; 779 } 780 781 getRecordsSizeForEf(PhonebookArgs args)782 private int[] getRecordsSizeForEf(PhonebookArgs args) { 783 try { 784 return mIccPhoneBookSupplier.get().getAdnRecordsSizeForSubscriber( 785 args.subscriptionId, args.efid); 786 } catch (RemoteException e) { 787 return null; 788 } 789 } 790 notifyChange()791 void notifyChange() { 792 mContentNotifier.notifyChange(SimPhonebookContract.AUTHORITY_URI); 793 } 794 795 /** Testable wrapper around {@link ContentResolver#notifyChange(Uri, ContentObserver)} */ 796 @TestApi 797 interface ContentNotifier { notifyChange(Uri uri)798 void notifyChange(Uri uri); 799 } 800 801 /** 802 * Holds the arguments extracted from the Uri and query args for accessing the referenced 803 * phonebook data on a SIM. 804 */ 805 private static class PhonebookArgs { 806 public final Uri uri; 807 public final int subscriptionId; 808 public final String efName; 809 public final int efType; 810 public final int efid; 811 public final int recordNumber; 812 public final String pin2; 813 PhonebookArgs(Uri uri, int subscriptionId, String efName, @ElementaryFiles.EfType int efType, int efid, int recordNumber, @Nullable Bundle queryArgs)814 PhonebookArgs(Uri uri, int subscriptionId, String efName, 815 @ElementaryFiles.EfType int efType, int efid, int recordNumber, 816 @Nullable Bundle queryArgs) { 817 this.uri = uri; 818 this.subscriptionId = subscriptionId; 819 this.efName = efName; 820 this.efType = efType; 821 this.efid = efid; 822 this.recordNumber = recordNumber; 823 pin2 = efType == ElementaryFiles.EF_FDN && queryArgs != null 824 ? queryArgs.getString(SimRecords.QUERY_ARG_PIN2) 825 : null; 826 } 827 createFromEfName(Uri uri, int subscriptionId, String efName, int recordNumber, @Nullable Bundle queryArgs)828 static PhonebookArgs createFromEfName(Uri uri, int subscriptionId, 829 String efName, int recordNumber, @Nullable Bundle queryArgs) { 830 int efType; 831 int efid; 832 if (efName != null) { 833 switch (efName) { 834 case ElementaryFiles.PATH_SEGMENT_EF_ADN: 835 efType = ElementaryFiles.EF_ADN; 836 efid = IccConstants.EF_ADN; 837 break; 838 case ElementaryFiles.PATH_SEGMENT_EF_FDN: 839 efType = ElementaryFiles.EF_FDN; 840 efid = IccConstants.EF_FDN; 841 break; 842 case ElementaryFiles.PATH_SEGMENT_EF_SDN: 843 efType = ElementaryFiles.EF_SDN; 844 efid = IccConstants.EF_SDN; 845 break; 846 default: 847 throw new IllegalArgumentException( 848 "Unrecognized elementary file " + efName); 849 } 850 } else { 851 efType = ElementaryFiles.EF_UNKNOWN; 852 efid = 0; 853 } 854 return new PhonebookArgs(uri, subscriptionId, efName, efType, efid, recordNumber, 855 queryArgs); 856 } 857 858 /** 859 * Pattern: elementary_files/subid/${subscriptionId}/${efName} 860 * 861 * e.g. elementary_files/subid/1/adn 862 * 863 * @see ElementaryFiles#getItemUri(int, int) 864 * @see #ELEMENTARY_FILES_ITEM 865 */ forElementaryFilesItem(Uri uri)866 static PhonebookArgs forElementaryFilesItem(Uri uri) { 867 int subscriptionId = parseSubscriptionIdFromUri(uri, 2); 868 String efName = uri.getPathSegments().get(3); 869 return PhonebookArgs.createFromEfName( 870 uri, subscriptionId, efName, -1, null); 871 } 872 873 /** 874 * Pattern: subid/${subscriptionId}/${efName} 875 * 876 * <p>e.g. subid/1/adn 877 * 878 * @see SimRecords#getContentUri(int, int) 879 * @see #SIM_RECORDS 880 */ forSimRecords(Uri uri, Bundle queryArgs)881 static PhonebookArgs forSimRecords(Uri uri, Bundle queryArgs) { 882 int subscriptionId = parseSubscriptionIdFromUri(uri, 1); 883 String efName = uri.getPathSegments().get(2); 884 return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, -1, queryArgs); 885 } 886 887 /** 888 * Pattern: subid/${subscriptionId}/${efName}/${recordNumber} 889 * 890 * <p>e.g. subid/1/adn/10 891 * 892 * @see SimRecords#getItemUri(int, int, int) 893 * @see #SIM_RECORDS_ITEM 894 */ forSimRecordsItem(Uri uri, Bundle queryArgs)895 static PhonebookArgs forSimRecordsItem(Uri uri, Bundle queryArgs) { 896 int subscriptionId = parseSubscriptionIdFromUri(uri, 1); 897 String efName = uri.getPathSegments().get(2); 898 int recordNumber = parseRecordNumberFromUri(uri, 3); 899 return PhonebookArgs.createFromEfName(uri, subscriptionId, efName, recordNumber, 900 queryArgs); 901 } 902 parseSubscriptionIdFromUri(Uri uri, int pathIndex)903 private static int parseSubscriptionIdFromUri(Uri uri, int pathIndex) { 904 if (pathIndex == -1) { 905 return SubscriptionManager.INVALID_SUBSCRIPTION_ID; 906 } 907 String segment = uri.getPathSegments().get(pathIndex); 908 try { 909 return Integer.parseInt(segment); 910 } catch (NumberFormatException e) { 911 throw new IllegalArgumentException("Invalid subscription ID: " + segment); 912 } 913 } 914 parseRecordNumberFromUri(Uri uri, int pathIndex)915 private static int parseRecordNumberFromUri(Uri uri, int pathIndex) { 916 try { 917 return Integer.parseInt(uri.getPathSegments().get(pathIndex)); 918 } catch (NumberFormatException e) { 919 throw new IllegalArgumentException( 920 "Invalid record index: " + uri.getLastPathSegment()); 921 } 922 } 923 } 924 } 925