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