• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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