1 /* 2 * Copyright (C) 2018 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.settings.network; 18 19 import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX; 20 import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT; 21 import static com.android.internal.util.CollectionUtils.emptyIfNull; 22 23 import android.annotation.Nullable; 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.os.ParcelUuid; 27 import android.provider.Settings; 28 import android.telephony.PhoneNumberUtils; 29 import android.telephony.SubscriptionInfo; 30 import android.telephony.SubscriptionManager; 31 import android.telephony.TelephonyManager; 32 import android.telephony.UiccCardInfo; 33 import android.telephony.UiccSlotInfo; 34 import android.text.BidiFormatter; 35 import android.text.TextDirectionHeuristics; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import androidx.annotation.VisibleForTesting; 40 41 import com.android.internal.telephony.MccTable; 42 import com.android.settings.R; 43 import com.android.settings.network.helper.SelectableSubscriptions; 44 import com.android.settings.network.helper.SubscriptionAnnotation; 45 import com.android.settings.network.telephony.DeleteEuiccSubscriptionDialogActivity; 46 import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.Map; 55 import java.util.Set; 56 import java.util.function.Function; 57 import java.util.function.Supplier; 58 import java.util.regex.Matcher; 59 import java.util.regex.Pattern; 60 import java.util.stream.Collectors; 61 import java.util.stream.Stream; 62 63 public class SubscriptionUtil { 64 private static final String TAG = "SubscriptionUtil"; 65 private static final String PROFILE_GENERIC_DISPLAY_NAME = "CARD"; 66 @VisibleForTesting 67 static final String SUB_ID = "sub_id"; 68 @VisibleForTesting 69 static final String KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME = "unique_subscription_displayName"; 70 private static final String REGEX_DISPLAY_NAME_PREFIXES = "^"; 71 private static final String REGEX_DISPLAY_NAME_SUFFIXES = "\\s[0-9]+"; 72 73 private static List<SubscriptionInfo> sAvailableResultsForTesting; 74 private static List<SubscriptionInfo> sActiveResultsForTesting; 75 76 @VisibleForTesting setAvailableSubscriptionsForTesting(List<SubscriptionInfo> results)77 public static void setAvailableSubscriptionsForTesting(List<SubscriptionInfo> results) { 78 sAvailableResultsForTesting = results; 79 } 80 81 @VisibleForTesting setActiveSubscriptionsForTesting(List<SubscriptionInfo> results)82 public static void setActiveSubscriptionsForTesting(List<SubscriptionInfo> results) { 83 sActiveResultsForTesting = results; 84 } 85 getActiveSubscriptions(SubscriptionManager manager)86 public static List<SubscriptionInfo> getActiveSubscriptions(SubscriptionManager manager) { 87 if (sActiveResultsForTesting != null) { 88 return sActiveResultsForTesting; 89 } 90 if (manager == null) { 91 return Collections.emptyList(); 92 } 93 final List<SubscriptionInfo> subscriptions = manager.getActiveSubscriptionInfoList(); 94 if (subscriptions == null) { 95 return new ArrayList<>(); 96 } 97 return subscriptions; 98 } 99 100 /** 101 * Check if SIM hardware is visible to the end user. 102 */ isSimHardwareVisible(Context context)103 public static boolean isSimHardwareVisible(Context context) { 104 return context.getResources() 105 .getBoolean(R.bool.config_show_sim_info); 106 } 107 108 @VisibleForTesting isInactiveInsertedPSim(UiccSlotInfo slotInfo)109 static boolean isInactiveInsertedPSim(UiccSlotInfo slotInfo) { 110 if (slotInfo == null) { 111 return false; 112 } 113 return !slotInfo.getIsEuicc() && !slotInfo.getPorts().stream().findFirst().get() 114 .isActive() && slotInfo.getCardStateInfo() == CARD_STATE_INFO_PRESENT; 115 } 116 117 /** 118 * Get all of the subscriptions which is available to display to the user. 119 * 120 * @param context {@code Context} 121 * @return list of {@code SubscriptionInfo} 122 */ getAvailableSubscriptions(Context context)123 public static List<SubscriptionInfo> getAvailableSubscriptions(Context context) { 124 if (sAvailableResultsForTesting != null) { 125 return sAvailableResultsForTesting; 126 } 127 return new ArrayList<>(emptyIfNull(getSelectableSubscriptionInfoList(context))); 128 } 129 130 /** 131 * Get subscription which is available to be displayed to the user 132 * per subscription id. 133 * 134 * @param context {@code Context} 135 * @param subscriptionManager The ProxySubscriptionManager for accessing subcription 136 * information 137 * @param subId The id of subscription to be retrieved 138 * @return {@code SubscriptionInfo} based on the given subscription id. Null of subscription 139 * is invalid or not allowed to be displayed to the user. 140 */ getAvailableSubscription(Context context, ProxySubscriptionManager subscriptionManager, int subId)141 public static SubscriptionInfo getAvailableSubscription(Context context, 142 ProxySubscriptionManager subscriptionManager, int subId) { 143 final SubscriptionInfo subInfo = subscriptionManager.getAccessibleSubscriptionInfo(subId); 144 if (subInfo == null) { 145 return null; 146 } 147 148 final ParcelUuid groupUuid = subInfo.getGroupUuid(); 149 150 if (groupUuid != null) { 151 if (isPrimarySubscriptionWithinSameUuid(getUiccSlotsInfo(context), groupUuid, 152 subscriptionManager.getAccessibleSubscriptionsInfo(), subId)) { 153 return subInfo; 154 } 155 return null; 156 } 157 158 return subInfo; 159 } 160 getUiccSlotsInfo(Context context)161 private static UiccSlotInfo [] getUiccSlotsInfo(Context context) { 162 final TelephonyManager telMgr = context.getSystemService(TelephonyManager.class); 163 return telMgr.getUiccSlotsInfo(); 164 } 165 isPrimarySubscriptionWithinSameUuid(UiccSlotInfo[] slotsInfo, ParcelUuid groupUuid, List<SubscriptionInfo> subscriptions, int subId)166 private static boolean isPrimarySubscriptionWithinSameUuid(UiccSlotInfo[] slotsInfo, 167 ParcelUuid groupUuid, List<SubscriptionInfo> subscriptions, int subId) { 168 // only interested in subscriptions with this group UUID 169 final ArrayList<SubscriptionInfo> physicalSubInfoList = 170 new ArrayList<SubscriptionInfo>(); 171 final ArrayList<SubscriptionInfo> nonOpportunisticSubInfoList = 172 new ArrayList<SubscriptionInfo>(); 173 final ArrayList<SubscriptionInfo> activeSlotSubInfoList = 174 new ArrayList<SubscriptionInfo>(); 175 final ArrayList<SubscriptionInfo> inactiveSlotSubInfoList = 176 new ArrayList<SubscriptionInfo>(); 177 for (SubscriptionInfo subInfo : subscriptions) { 178 if (groupUuid.equals(subInfo.getGroupUuid())) { 179 if (!subInfo.isEmbedded()) { 180 physicalSubInfoList.add(subInfo); 181 } else { 182 if (!subInfo.isOpportunistic()) { 183 nonOpportunisticSubInfoList.add(subInfo); 184 } 185 if (subInfo.getSimSlotIndex() 186 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { 187 activeSlotSubInfoList.add(subInfo); 188 } else { 189 inactiveSlotSubInfoList.add(subInfo); 190 } 191 } 192 } 193 } 194 195 // find any physical SIM which is currently inserted within logical slot 196 // and which is our target subscription 197 if ((slotsInfo != null) && (physicalSubInfoList.size() > 0)) { 198 final SubscriptionInfo subInfo = searchForSubscriptionId(physicalSubInfoList, subId); 199 if (subInfo == null) { 200 return false; 201 } 202 // verify if subscription is inserted within slot 203 for (UiccSlotInfo slotInfo : slotsInfo) { 204 if ((slotInfo != null) && (!slotInfo.getIsEuicc()) 205 && (slotInfo.getPorts().stream().findFirst().get().getLogicalSlotIndex() 206 == subInfo.getSimSlotIndex())) { 207 return true; 208 } 209 } 210 return false; 211 } 212 213 // When all of the eSIM profiles are opprtunistic and no physical SIM, 214 // first opportunistic subscriptions with same group UUID can be primary. 215 if (nonOpportunisticSubInfoList.size() <= 0) { 216 if (physicalSubInfoList.size() > 0) { 217 return false; 218 } 219 if (activeSlotSubInfoList.size() > 0) { 220 return (activeSlotSubInfoList.get(0).getSubscriptionId() == subId); 221 } 222 return (inactiveSlotSubInfoList.get(0).getSubscriptionId() == subId); 223 } 224 225 // Allow non-opportunistic + active eSIM subscription as primary 226 int numberOfActiveNonOpportunisticSubs = 0; 227 boolean isTargetNonOpportunistic = false; 228 for (SubscriptionInfo subInfo : nonOpportunisticSubInfoList) { 229 final boolean isTargetSubInfo = (subInfo.getSubscriptionId() == subId); 230 if (subInfo.getSimSlotIndex() != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { 231 if (isTargetSubInfo) { 232 return true; 233 } 234 numberOfActiveNonOpportunisticSubs++; 235 } else { 236 isTargetNonOpportunistic |= isTargetSubInfo; 237 } 238 } 239 if (numberOfActiveNonOpportunisticSubs > 0) { 240 return false; 241 } 242 return isTargetNonOpportunistic; 243 } 244 searchForSubscriptionId(List<SubscriptionInfo> subInfoList, int subscriptionId)245 private static SubscriptionInfo searchForSubscriptionId(List<SubscriptionInfo> subInfoList, 246 int subscriptionId) { 247 for (SubscriptionInfo subInfo : subInfoList) { 248 if (subInfo.getSubscriptionId() == subscriptionId) { 249 return subInfo; 250 } 251 } 252 return null; 253 } 254 255 /** 256 * Return a mapping of active subscription ids to display names. Each display name is 257 * guaranteed to be unique in the following manner: 258 * 1) If the original display name is not unique, the last four digits of the phone number 259 * will be appended. 260 * 2) If the phone number is not visible or the last four digits are shared with another 261 * subscription, the subscription id will be appended to the original display name. 262 * More details can be found at go/unique-sub-display-names. 263 * 264 * @return map of active subscription ids to display names. 265 */ 266 @VisibleForTesting getUniqueSubscriptionDisplayNames(Context context)267 public static Map<Integer, CharSequence> getUniqueSubscriptionDisplayNames(Context context) { 268 class DisplayInfo { 269 public SubscriptionInfo subscriptionInfo; 270 public CharSequence originalName; 271 public CharSequence uniqueName; 272 } 273 274 // Map of SubscriptionId to DisplayName 275 final Supplier<Stream<DisplayInfo>> originalInfos = 276 () -> getAvailableSubscriptions(context) 277 .stream() 278 .filter(i -> { 279 // Filter out null values. 280 return (i != null && i.getDisplayName() != null); 281 }) 282 .map(i -> { 283 DisplayInfo info = new DisplayInfo(); 284 info.subscriptionInfo = i; 285 String displayName = i.getDisplayName().toString(); 286 info.originalName = 287 TextUtils.equals(displayName, PROFILE_GENERIC_DISPLAY_NAME) 288 ? context.getResources().getString(R.string.sim_card) 289 : displayName.trim(); 290 return info; 291 }); 292 293 // TODO(goldmanj) consider using a map of DisplayName to SubscriptionInfos. 294 // A Unique set of display names 295 Set<CharSequence> uniqueNames = new HashSet<>(); 296 // Return the set of duplicate names 297 final Set<CharSequence> duplicateOriginalNames = originalInfos.get() 298 .filter(info -> !uniqueNames.add(info.originalName)) 299 .map(info -> info.originalName) 300 .collect(Collectors.toSet()); 301 302 // If a display name is duplicate, append the final 4 digits of the phone number. 303 // Creates a mapping of Subscription id to original display name + phone number display name 304 final Supplier<Stream<DisplayInfo>> uniqueInfos = () -> originalInfos.get().map(info -> { 305 int infoSubId = info.subscriptionInfo.getSubscriptionId(); 306 String cachedDisplayName = getDisplayNameFromSharedPreference( 307 context, infoSubId); 308 if (isValidCachedDisplayName(cachedDisplayName, info.originalName.toString())) { 309 Log.d(TAG, "use cached display name : for subId : " + infoSubId 310 + "cached display name : " + cachedDisplayName); 311 info.uniqueName = cachedDisplayName; 312 return info; 313 } else { 314 Log.d(TAG, "remove cached display name : " + infoSubId); 315 removeItemFromDisplayNameSharedPreference(context, infoSubId); 316 } 317 318 if (duplicateOriginalNames.contains(info.originalName)) { 319 // This may return null, if the user cannot view the phone number itself. 320 final String phoneNumber = getBidiFormattedPhoneNumber(context, 321 info.subscriptionInfo); 322 String lastFourDigits = ""; 323 if (phoneNumber != null) { 324 lastFourDigits = (phoneNumber.length() > 4) 325 ? phoneNumber.substring(phoneNumber.length() - 4) : phoneNumber; 326 } 327 if (TextUtils.isEmpty(lastFourDigits)) { 328 info.uniqueName = info.originalName; 329 } else { 330 info.uniqueName = info.originalName + " " + lastFourDigits; 331 Log.d(TAG, "Cache display name [" + info.uniqueName + "] for sub id " 332 + infoSubId); 333 saveDisplayNameToSharedPreference(context, infoSubId, info.uniqueName); 334 } 335 } else { 336 info.uniqueName = info.originalName; 337 } 338 return info; 339 }); 340 341 // Check uniqueness a second time. 342 // We might not have had permission to view the phone numbers. 343 // There might also be multiple phone numbers whose last 4 digits the same. 344 uniqueNames.clear(); 345 final Set<CharSequence> duplicatePhoneNames = uniqueInfos.get() 346 .filter(info -> !uniqueNames.add(info.uniqueName)) 347 .map(info -> info.uniqueName) 348 .collect(Collectors.toSet()); 349 350 return uniqueInfos.get().map(info -> { 351 if (duplicatePhoneNames.contains(info.uniqueName)) { 352 info.uniqueName = info.originalName + " " 353 + info.subscriptionInfo.getSubscriptionId(); 354 } 355 return info; 356 }).collect(Collectors.toMap( 357 info -> info.subscriptionInfo.getSubscriptionId(), 358 info -> info.uniqueName)); 359 } 360 361 /** 362 * Return the display name for a subscription id, which is guaranteed to be unique. 363 * The logic to create this name has the following order of operations: 364 * 1) If the original display name is not unique, the last four digits of the phone number 365 * will be appended. 366 * 2) If the phone number is not visible or the last four digits are shared with another 367 * subscription, the subscription id will be appended to the original display name. 368 * More details can be found at go/unique-sub-display-names. 369 * 370 * @return map of active subscription ids to display names. 371 */ 372 @VisibleForTesting 373 public static CharSequence getUniqueSubscriptionDisplayName( 374 Integer subscriptionId, Context context) { 375 final Map<Integer, CharSequence> displayNames = getUniqueSubscriptionDisplayNames(context); 376 return displayNames.getOrDefault(subscriptionId, ""); 377 } 378 379 /** 380 * Return the display name for a subscription, which is guaranteed to be unique. 381 * The logic to create this name has the following order of operations: 382 * 1) If the original display name is not unique, the last four digits of the phone number 383 * will be appended. 384 * 2) If the phone number is not visible or the last four digits are shared with another 385 * subscription, the subscription id will be appended to the original display name. 386 * More details can be found at go/unique-sub-display-names. 387 * 388 * @return map of active subscription ids to display names. 389 */ 390 public static CharSequence getUniqueSubscriptionDisplayName( 391 SubscriptionInfo info, Context context) { 392 if (info == null) { 393 return ""; 394 } 395 return getUniqueSubscriptionDisplayName(info.getSubscriptionId(), context); 396 } 397 398 399 private static SharedPreferences getDisplayNameSharedPreferences(Context context) { 400 return context.getSharedPreferences( 401 KEY_UNIQUE_SUBSCRIPTION_DISPLAYNAME, Context.MODE_PRIVATE); 402 } 403 404 private static SharedPreferences.Editor getDisplayNameSharedPreferenceEditor(Context context) { 405 return getDisplayNameSharedPreferences(context).edit(); 406 } 407 408 private static void saveDisplayNameToSharedPreference( 409 Context context, int subId, CharSequence displayName) { 410 getDisplayNameSharedPreferenceEditor(context) 411 .putString(SUB_ID + subId, String.valueOf(displayName)) 412 .apply(); 413 } 414 415 private static void removeItemFromDisplayNameSharedPreference(Context context, int subId) { 416 getDisplayNameSharedPreferenceEditor(context) 417 .remove(SUB_ID + subId) 418 .commit(); 419 } 420 421 private static String getDisplayNameFromSharedPreference(Context context, int subid) { 422 return getDisplayNameSharedPreferences(context).getString(SUB_ID + subid, ""); 423 } 424 425 @VisibleForTesting 426 static boolean isValidCachedDisplayName(String cachedDisplayName, String originalName) { 427 if (TextUtils.isEmpty(cachedDisplayName) || TextUtils.isEmpty(originalName)) { 428 return false; 429 } 430 String regex = REGEX_DISPLAY_NAME_PREFIXES + originalName + REGEX_DISPLAY_NAME_SUFFIXES; 431 Pattern pattern = Pattern.compile(regex); 432 Matcher matcher = pattern.matcher(cachedDisplayName); 433 return matcher.matches(); 434 } 435 436 public static String getDisplayName(SubscriptionInfo info) { 437 final CharSequence name = info.getDisplayName(); 438 if (name != null) { 439 return name.toString(); 440 } 441 return ""; 442 } 443 444 /** 445 * Whether Settings should show a "Use SIM" toggle in pSIM detailed page. 446 */ 447 public static boolean showToggleForPhysicalSim(SubscriptionManager subMgr) { 448 return subMgr.canDisablePhysicalSubscription(); 449 } 450 451 /** 452 * Get phoneId or logical slot index for a subId if active, or INVALID_PHONE_INDEX if inactive. 453 */ 454 public static int getPhoneId(Context context, int subId) { 455 final SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); 456 if (subManager == null) { 457 return INVALID_SIM_SLOT_INDEX; 458 } 459 final SubscriptionInfo info = subManager.getActiveSubscriptionInfo(subId); 460 if (info == null) { 461 return INVALID_SIM_SLOT_INDEX; 462 } 463 return info.getSimSlotIndex(); 464 } 465 466 /** 467 * Return a list of subscriptions that are available and visible to the user. 468 * 469 * @return list of user selectable subscriptions. 470 */ 471 public static List<SubscriptionInfo> getSelectableSubscriptionInfoList(Context context) { 472 SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); 473 List<SubscriptionInfo> availableList = subManager.getAvailableSubscriptionInfoList(); 474 if (availableList == null) { 475 return null; 476 } else { 477 // Multiple subscriptions in a group should only have one representative. 478 // It should be the current active primary subscription if any, or any 479 // primary subscription. 480 List<SubscriptionInfo> selectableList = new ArrayList<>(); 481 Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>(); 482 483 for (SubscriptionInfo info : availableList) { 484 // Opportunistic subscriptions are considered invisible 485 // to users so they should never be returned. 486 if (!isSubscriptionVisible(subManager, context, info)) continue; 487 488 ParcelUuid groupUuid = info.getGroupUuid(); 489 if (groupUuid == null) { 490 // Doesn't belong to any group. Add in the list. 491 selectableList.add(info); 492 } else if (!groupMap.containsKey(groupUuid) 493 || (groupMap.get(groupUuid).getSimSlotIndex() == INVALID_SIM_SLOT_INDEX 494 && info.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX)) { 495 // If it belongs to a group that has never been recorded or it's the current 496 // active subscription, add it in the list. 497 selectableList.remove(groupMap.get(groupUuid)); 498 selectableList.add(info); 499 groupMap.put(groupUuid, info); 500 } 501 502 } 503 Log.d(TAG, "getSelectableSubscriptionInfoList: " + selectableList); 504 return selectableList; 505 } 506 } 507 508 /** 509 * Starts a dialog activity to handle SIM enabling/disabling. 510 * @param context {@code Context} 511 * @param subId The id of subscription need to be enabled or disabled. 512 * @param enable Whether the subscription with {@code subId} should be enabled or disabled. 513 */ 514 public static void startToggleSubscriptionDialogActivity( 515 Context context, int subId, boolean enable) { 516 if (!SubscriptionManager.isUsableSubscriptionId(subId)) { 517 Log.i(TAG, "Unable to toggle subscription due to invalid subscription ID."); 518 return; 519 } 520 context.startActivity(ToggleSubscriptionDialogActivity.getIntent(context, subId, enable)); 521 } 522 523 /** 524 * Starts a dialog activity to handle eSIM deletion. 525 * @param context {@code Context} 526 * @param subId The id of subscription need to be deleted. 527 */ 528 public static void startDeleteEuiccSubscriptionDialogActivity(Context context, int subId) { 529 if (!SubscriptionManager.isUsableSubscriptionId(subId)) { 530 Log.i(TAG, "Unable to delete subscription due to invalid subscription ID."); 531 return; 532 } 533 context.startActivity(DeleteEuiccSubscriptionDialogActivity.getIntent(context, subId)); 534 } 535 536 /** 537 * Finds and returns a subscription with a specific subscription ID. 538 * @param subscriptionManager The ProxySubscriptionManager for accessing subscription 539 * information 540 * @param subId The id of subscription to be returned 541 * @return the {@code SubscriptionInfo} whose ID is {@code subId}. It returns null if the 542 * {@code subId} is {@code SubscriptionManager.INVALID_SUBSCRIPTION_ID} or no such 543 * {@code SubscriptionInfo} is found. 544 */ 545 @Nullable 546 public static SubscriptionInfo getSubById(SubscriptionManager subscriptionManager, int subId) { 547 if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 548 return null; 549 } 550 return subscriptionManager 551 .getAllSubscriptionInfoList() 552 .stream() 553 .filter(subInfo -> subInfo.getSubscriptionId() == subId) 554 .findFirst() 555 .orElse(null); 556 } 557 558 /** 559 * Whether a subscription is visible to API caller. If it's a bundled opportunistic 560 * subscription, it should be hidden anywhere in Settings, dialer, status bar etc. 561 * Exception is if caller owns carrier privilege, in which case they will 562 * want to see their own hidden subscriptions. 563 * 564 * @param info the subscriptionInfo to check against. 565 * @return true if this subscription should be visible to the API caller. 566 */ 567 public static boolean isSubscriptionVisible( 568 SubscriptionManager subscriptionManager, Context context, SubscriptionInfo info) { 569 if (info == null) return false; 570 // If subscription is NOT grouped opportunistic subscription, it's visible. 571 if (info.getGroupUuid() == null || !info.isOpportunistic()) return true; 572 573 // If the caller is the carrier app and owns the subscription, it should be visible 574 // to the caller. 575 TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) 576 .createForSubscriptionId(info.getSubscriptionId()); 577 boolean hasCarrierPrivilegePermission = telephonyManager.hasCarrierPrivileges() 578 || subscriptionManager.canManageSubscription(info); 579 return hasCarrierPrivilegePermission; 580 } 581 582 /** 583 * Finds all the available subscriptions having the same group uuid as {@code subscriptionInfo}. 584 * @param subscriptionManager The SubscriptionManager for accessing subscription information 585 * @param subId The id of subscription 586 * @return a list of {@code SubscriptionInfo} which have the same group UUID. 587 */ 588 public static List<SubscriptionInfo> findAllSubscriptionsInGroup( 589 SubscriptionManager subscriptionManager, int subId) { 590 591 SubscriptionInfo subscription = getSubById(subscriptionManager, subId); 592 if (subscription == null) { 593 return Collections.emptyList(); 594 } 595 ParcelUuid groupUuid = subscription.getGroupUuid(); 596 List<SubscriptionInfo> availableSubscriptions = 597 subscriptionManager.getAvailableSubscriptionInfoList(); 598 599 if (availableSubscriptions == null 600 || availableSubscriptions.isEmpty() 601 || groupUuid == null) { 602 return Collections.singletonList(subscription); 603 } 604 605 return availableSubscriptions 606 .stream() 607 .filter(sub -> sub.isEmbedded() && groupUuid.equals(sub.getGroupUuid())) 608 .collect(Collectors.toList()); 609 } 610 611 /** Returns the formatted phone number of a subscription. */ 612 @Nullable 613 public static String getFormattedPhoneNumber( 614 Context context, SubscriptionInfo subscriptionInfo) { 615 if (subscriptionInfo == null) { 616 Log.e(TAG, "Invalid subscription."); 617 return null; 618 } 619 620 final SubscriptionManager subscriptionManager = context.getSystemService( 621 SubscriptionManager.class); 622 String rawPhoneNumber = subscriptionManager.getPhoneNumber( 623 subscriptionInfo.getSubscriptionId()); 624 if (TextUtils.isEmpty(rawPhoneNumber)) { 625 return null; 626 } 627 String countryIso = MccTable.countryCodeForMcc(subscriptionInfo.getMccString()) 628 .toUpperCase(Locale.ROOT); 629 return PhoneNumberUtils.formatNumber(rawPhoneNumber, countryIso); 630 } 631 632 /** 633 * To get the formatting text for display in a potentially opposite-directionality context 634 * without garbling. 635 * @param subscriptionInfo {@link SubscriptionInfo} subscription information. 636 * @return Returns phone number with Bidi format. 637 */ 638 @Nullable 639 public static String getBidiFormattedPhoneNumber(Context context, 640 SubscriptionInfo subscriptionInfo) { 641 String phoneNumber = getFormattedPhoneNumber(context, subscriptionInfo); 642 return (phoneNumber == null) ? phoneNumber : 643 BidiFormatter.getInstance().unicodeWrap(phoneNumber, TextDirectionHeuristics.LTR); 644 } 645 646 /** 647 * Returns the subscription on a removable sim card. The device does not need to be on removable 648 * slot. 649 */ 650 @Nullable 651 public static SubscriptionInfo getFirstRemovableSubscription(Context context) { 652 TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); 653 SubscriptionManager subscriptionManager = 654 context.getSystemService(SubscriptionManager.class); 655 List<UiccCardInfo> cardInfos = telephonyManager.getUiccCardsInfo(); 656 if (cardInfos == null) { 657 Log.w(TAG, "UICC cards info list is empty."); 658 return null; 659 } 660 List<SubscriptionInfo> allSubscriptions = subscriptionManager.getAllSubscriptionInfoList(); 661 if (allSubscriptions == null) { 662 Log.w(TAG, "All subscription info list is empty."); 663 return null; 664 } 665 for (UiccCardInfo cardInfo : cardInfos) { 666 if (cardInfo == null) { 667 Log.w(TAG, "Got null card."); 668 continue; 669 } 670 if (!cardInfo.isRemovable() 671 || cardInfo.getCardId() == TelephonyManager.UNSUPPORTED_CARD_ID) { 672 Log.i(TAG, "Skip embedded card or invalid cardId on slot: " 673 + cardInfo.getPhysicalSlotIndex()); 674 continue; 675 } 676 Log.i(TAG, "Target removable cardId :" + cardInfo.getCardId()); 677 for (SubscriptionInfo subInfo : allSubscriptions) { 678 // Match the removable card id with subscription card id. 679 if (cardInfo.getCardId() == subInfo.getCardId()) { 680 return subInfo; 681 } 682 } 683 } 684 return null; 685 } 686 687 public static CharSequence getDefaultSimConfig(Context context, int subId) { 688 boolean isDefaultCall = subId == getDefaultVoiceSubscriptionId(); 689 boolean isDefaultSms = subId == getDefaultSmsSubscriptionId(); 690 boolean isDefaultData = subId == getDefaultDataSubscriptionId(); 691 692 if (!isDefaultData && !isDefaultCall && !isDefaultSms) { 693 return ""; 694 } 695 696 final StringBuilder defaultConfig = new StringBuilder(); 697 if (isDefaultData) { 698 defaultConfig.append( 699 getResForDefaultConfig(context, R.string.default_active_sim_mobile_data)) 700 .append(", "); 701 } 702 703 if (isDefaultCall) { 704 defaultConfig.append(getResForDefaultConfig(context, R.string.default_active_sim_calls)) 705 .append(", "); 706 } 707 708 if (isDefaultSms) { 709 defaultConfig.append(getResForDefaultConfig(context, R.string.default_active_sim_sms)) 710 .append(", "); 711 } 712 713 // Do not add ", " for the last config. 714 defaultConfig.setLength(defaultConfig.length() - 2); 715 716 final String summary = context.getResources().getString( 717 R.string.sim_category_default_active_sim, 718 defaultConfig); 719 720 return summary; 721 } 722 723 private static String getResForDefaultConfig(Context context, int resId) { 724 return context.getResources().getString(resId); 725 } 726 727 private static int getDefaultVoiceSubscriptionId() { 728 return SubscriptionManager.getDefaultVoiceSubscriptionId(); 729 } 730 731 private static int getDefaultSmsSubscriptionId() { 732 return SubscriptionManager.getDefaultSmsSubscriptionId(); 733 } 734 735 private static int getDefaultDataSubscriptionId() { 736 return SubscriptionManager.getDefaultDataSubscriptionId(); 737 } 738 739 740 /** 741 * Select one of the subscription as the default subscription. 742 * @param subAnnoList a list of {@link SubscriptionAnnotation} 743 * @return ideally the {@link SubscriptionAnnotation} as expected 744 */ 745 private static SubscriptionAnnotation getDefaultSubscriptionSelection( 746 List<SubscriptionAnnotation> subAnnoList) { 747 return (subAnnoList == null) ? null : 748 subAnnoList.stream() 749 .filter(SubscriptionAnnotation::isDisplayAllowed) 750 .filter(SubscriptionAnnotation::isActive) 751 .findFirst().orElse(null); 752 } 753 754 public static boolean isDefaultSubscription(Context context, int subId) { 755 SubscriptionAnnotation subInfo = getDefaultSubscriptionSelection( 756 new SelectableSubscriptions(context, true).call()); 757 return subInfo != null && subInfo.getSubscriptionId() == subId; 758 } 759 760 public static SubscriptionInfo getSubscriptionOrDefault(Context context, int subscriptionId) { 761 return getSubscription(context, subscriptionId, 762 (subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) ? null : ( 763 subAnnoList -> getDefaultSubscriptionSelection(subAnnoList) 764 )); 765 } 766 767 /** 768 * Get the current subscription to display. First check whether intent has {@link 769 * Settings#EXTRA_SUB_ID} and if so find the subscription with that id. 770 * If not, select default one based on {@link Function} provided. 771 * 772 * @param preferredSubscriptionId preferred subscription id 773 * @param selectionOfDefault when true current subscription is absent 774 */ 775 private static SubscriptionInfo getSubscription(Context context, int preferredSubscriptionId, 776 Function<List<SubscriptionAnnotation>, SubscriptionAnnotation> selectionOfDefault) { 777 List<SubscriptionAnnotation> subList = 778 (new SelectableSubscriptions(context, true)).call(); 779 Log.d(TAG, "get subId=" + preferredSubscriptionId + " from " + subList); 780 SubscriptionAnnotation currentSubInfo = subList.stream() 781 .filter(SubscriptionAnnotation::isDisplayAllowed) 782 .filter(subAnno -> (subAnno.getSubscriptionId() == preferredSubscriptionId)) 783 .findFirst().orElse(null); 784 if ((currentSubInfo == null) && (selectionOfDefault != null)) { 785 currentSubInfo = selectionOfDefault.apply(subList); 786 } 787 return (currentSubInfo == null) ? null : currentSubInfo.getSubInfo(); 788 } 789 } 790