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