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