1 /*
2  * Copyright (C) 2019 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.wifitrackerlib;
18 
19 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
20 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED;
21 
22 import static com.android.wifitrackerlib.WifiEntry.SPEED_FAST;
23 import static com.android.wifitrackerlib.WifiEntry.SPEED_MODERATE;
24 import static com.android.wifitrackerlib.WifiEntry.SPEED_NONE;
25 import static com.android.wifitrackerlib.WifiEntry.SPEED_SLOW;
26 import static com.android.wifitrackerlib.WifiEntry.SPEED_VERY_FAST;
27 import static com.android.wifitrackerlib.WifiEntry.Speed;
28 
29 import static java.util.Comparator.comparingInt;
30 
31 import android.content.Context;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageManager;
34 import android.net.NetworkCapabilities;
35 import android.net.NetworkInfo;
36 import android.net.NetworkInfo.DetailedState;
37 import android.net.NetworkKey;
38 import android.net.ScoredNetwork;
39 import android.net.WifiKey;
40 import android.net.wifi.ScanResult;
41 import android.net.wifi.WifiConfiguration;
42 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
43 import android.net.wifi.WifiInfo;
44 import android.net.wifi.WifiNetworkScoreCache;
45 import android.os.PersistableBundle;
46 import android.telephony.CarrierConfigManager;
47 import android.telephony.SubscriptionInfo;
48 import android.telephony.SubscriptionManager;
49 import android.telephony.TelephonyManager;
50 import android.text.Annotation;
51 import android.text.SpannableString;
52 import android.text.SpannableStringBuilder;
53 import android.text.TextUtils;
54 import android.text.format.DateUtils;
55 import android.text.style.ClickableSpan;
56 import android.view.View;
57 
58 import androidx.annotation.NonNull;
59 import androidx.annotation.Nullable;
60 
61 import com.android.settingslib.HelpUtils;
62 
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Collections;
66 import java.util.List;
67 import java.util.StringJoiner;
68 
69 /**
70  * Utility methods for WifiTrackerLib.
71  */
72 public class Utils {
73     // Returns the ScanResult with the best RSSI from a list of ScanResults.
74     @Nullable
getBestScanResultByLevel(@onNull List<ScanResult> scanResults)75     public static ScanResult getBestScanResultByLevel(@NonNull List<ScanResult> scanResults) {
76         if (scanResults.isEmpty()) return null;
77 
78         return Collections.max(scanResults, comparingInt(scanResult -> scanResult.level));
79     }
80 
81     // Returns a list of WifiInfo SECURITY_TYPE_* supported by a ScanResult.
82     // TODO(b/187755981): Move to shared static utils class
83     @NonNull
getSecurityTypesFromScanResult(@onNull ScanResult scanResult)84     static List<Integer> getSecurityTypesFromScanResult(@NonNull ScanResult scanResult) {
85         List<Integer> securityTypes = new ArrayList<>();
86 
87         // Open network & its upgradable types
88         if (isScanResultForOweTransitionNetwork(scanResult)) {
89             securityTypes.add(WifiInfo.SECURITY_TYPE_OPEN);
90             securityTypes.add(WifiInfo.SECURITY_TYPE_OWE);
91             return securityTypes;
92         } else if (isScanResultForOweNetwork(scanResult)) {
93             securityTypes.add(WifiInfo.SECURITY_TYPE_OWE);
94             return securityTypes;
95         } else if (isScanResultForOpenNetwork(scanResult)) {
96             securityTypes.add(WifiInfo.SECURITY_TYPE_OPEN);
97             return securityTypes;
98         }
99 
100         // WEP network which has no upgradable type
101         if (isScanResultForWepNetwork(scanResult)) {
102             securityTypes.add(WifiInfo.SECURITY_TYPE_WEP);
103             return securityTypes;
104         }
105 
106         // WAPI PSK network which has no upgradable type
107         if (isScanResultForWapiPskNetwork(scanResult)) {
108             securityTypes.add(WifiInfo.SECURITY_TYPE_WAPI_PSK);
109             return securityTypes;
110         }
111 
112         // WAPI CERT network which has no upgradable type
113         if (isScanResultForWapiCertNetwork(scanResult)) {
114             securityTypes.add(
115                     WifiInfo.SECURITY_TYPE_WAPI_CERT);
116             return securityTypes;
117         }
118 
119         // WPA2 personal network & its upgradable types
120         if (isScanResultForPskNetwork(scanResult)
121                 && isScanResultForSaeNetwork(scanResult)) {
122             securityTypes.add(WifiInfo.SECURITY_TYPE_PSK);
123             securityTypes.add(WifiInfo.SECURITY_TYPE_SAE);
124             return securityTypes;
125         } else if (isScanResultForPskNetwork(scanResult)) {
126             securityTypes.add(WifiInfo.SECURITY_TYPE_PSK);
127             return securityTypes;
128         } else if (isScanResultForSaeNetwork(scanResult)) {
129             securityTypes.add(WifiInfo.SECURITY_TYPE_SAE);
130             return securityTypes;
131         }
132 
133         // WPA3 Enterprise 192-bit mode, WPA2/WPA3 enterprise network & its upgradable types
134         if (isScanResultForEapSuiteBNetwork(scanResult)) {
135             securityTypes.add(WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT);
136         } else if (isScanResultForWpa3EnterpriseTransitionNetwork(scanResult)) {
137             securityTypes.add(WifiInfo.SECURITY_TYPE_EAP);
138             securityTypes.add(WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
139         } else if (isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)) {
140             securityTypes.add(WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
141         } else if (isScanResultForEapNetwork(scanResult)) {
142             securityTypes.add(WifiInfo.SECURITY_TYPE_EAP);
143         }
144         return securityTypes;
145     }
146 
147     // Returns a list of WifiInfo SECURITY_TYPE_* supported by a WifiConfiguration
148     // TODO(b/187755473): Use new public APIs to get the security type instead of relying on the
149     //                    legacy allowedKeyManagement bitset.
getSecurityTypesFromWifiConfiguration(@onNull WifiConfiguration config)150     static List<Integer> getSecurityTypesFromWifiConfiguration(@NonNull WifiConfiguration config) {
151         if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WAPI_CERT)) {
152             return Arrays.asList(WifiInfo.SECURITY_TYPE_WAPI_CERT);
153         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WAPI_PSK)) {
154             return Arrays.asList(WifiInfo.SECURITY_TYPE_WAPI_PSK);
155         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SUITE_B_192)) {
156             return Arrays.asList(WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT);
157         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.OWE)) {
158             return Arrays.asList(WifiInfo.SECURITY_TYPE_OWE);
159         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
160             return Arrays.asList(WifiInfo.SECURITY_TYPE_SAE);
161         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA2_PSK)) {
162             return Arrays.asList(WifiInfo.SECURITY_TYPE_PSK);
163         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
164             if (config.requirePmf
165                     && !config.allowedPairwiseCiphers.get(WifiConfiguration.PairwiseCipher.TKIP)
166                     && config.allowedProtocols.get(WifiConfiguration.Protocol.RSN)) {
167                 return Arrays.asList(WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
168             } else {
169                 // WPA2 configs should also be valid for WPA3-Enterprise APs
170                 return Arrays.asList(
171                         WifiInfo.SECURITY_TYPE_EAP, WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
172             }
173         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
174             return Arrays.asList(WifiInfo.SECURITY_TYPE_PSK);
175         } else if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.NONE)) {
176             if (config.wepKeys != null) {
177                 for (int i = 0; i < config.wepKeys.length; i++) {
178                     if (config.wepKeys[i] != null) {
179                         return Arrays.asList(WifiInfo.SECURITY_TYPE_WEP);
180                     }
181                 }
182             }
183         }
184         return Arrays.asList(WifiInfo.SECURITY_TYPE_OPEN);
185     }
186 
187     /**
188      * Returns a single WifiInfo security type from the list of multiple WifiInfo security
189      * types supported by an entry.
190      *
191      * Single security types will have a 1-to-1 mapping.
192      * Multiple security type networks will collapse to the lowest security type in the group:
193      *     - Open/OWE -> Open
194      *     - PSK/SAE -> PSK
195      *     - EAP/EAP-WPA3 -> EAP
196      */
getSingleSecurityTypeFromMultipleSecurityTypes( @onNull List<Integer> securityTypes)197     static int getSingleSecurityTypeFromMultipleSecurityTypes(
198             @NonNull List<Integer> securityTypes) {
199         if (securityTypes.size() == 1) {
200             return securityTypes.get(0);
201         } else if (securityTypes.size() == 2) {
202             if (securityTypes.contains(WifiInfo.SECURITY_TYPE_OPEN)) {
203                 return WifiInfo.SECURITY_TYPE_OPEN;
204             }
205             if (securityTypes.contains(WifiInfo.SECURITY_TYPE_PSK)) {
206                 return WifiInfo.SECURITY_TYPE_PSK;
207             }
208             if (securityTypes.contains(WifiInfo.SECURITY_TYPE_EAP)) {
209                 return WifiInfo.SECURITY_TYPE_EAP;
210             }
211         }
212         return WifiInfo.SECURITY_TYPE_UNKNOWN;
213     }
214 
215     @Speed
getAverageSpeedFromScanResults(@onNull WifiNetworkScoreCache scoreCache, @NonNull List<ScanResult> scanResults)216     public static int getAverageSpeedFromScanResults(@NonNull WifiNetworkScoreCache scoreCache,
217             @NonNull List<ScanResult> scanResults) {
218         int count = 0;
219         int totalSpeed = 0;
220         for (ScanResult scanResult : scanResults) {
221             ScoredNetwork scoredNetwork = scoreCache.getScoredNetwork(scanResult);
222             if (scoredNetwork == null) {
223                 continue;
224             }
225             @Speed int speed = scoredNetwork.calculateBadge(scanResult.level);
226             if (speed != SPEED_NONE) {
227                 count++;
228                 totalSpeed += speed;
229             }
230         }
231         if (count == 0) {
232             return SPEED_NONE;
233         } else {
234             return roundToClosestSpeedEnum(totalSpeed / count);
235         }
236     }
237 
238     @Speed
getSpeedFromWifiInfo(@onNull WifiNetworkScoreCache scoreCache, @NonNull WifiInfo wifiInfo)239     public static int getSpeedFromWifiInfo(@NonNull WifiNetworkScoreCache scoreCache,
240             @NonNull WifiInfo wifiInfo) {
241         final WifiKey wifiKey;
242         try {
243             wifiKey = new WifiKey(wifiInfo.getSSID(), wifiInfo.getBSSID());
244         } catch (IllegalArgumentException e) {
245             return SPEED_NONE;
246         }
247         ScoredNetwork scoredNetwork = scoreCache.getScoredNetwork(
248                 new NetworkKey(wifiKey));
249         if (scoredNetwork == null) {
250             return SPEED_NONE;
251         }
252         return roundToClosestSpeedEnum(scoredNetwork.calculateBadge(wifiInfo.getRssi()));
253     }
254 
255     @Speed
roundToClosestSpeedEnum(int speed)256     private static int roundToClosestSpeedEnum(int speed) {
257         if (speed == SPEED_NONE) {
258             return SPEED_NONE;
259         } else if (speed < (SPEED_SLOW + SPEED_MODERATE) / 2) {
260             return SPEED_SLOW;
261         } else if (speed < (SPEED_MODERATE + SPEED_FAST) / 2) {
262             return SPEED_MODERATE;
263         } else if (speed < (SPEED_FAST + SPEED_VERY_FAST) / 2) {
264             return SPEED_FAST;
265         } else {
266             return SPEED_VERY_FAST;
267         }
268     }
269 
270     /**
271      * Get the app label for a suggestion/specifier package name, or an empty String if none exist
272      */
getAppLabel(Context context, String packageName)273     static String getAppLabel(Context context, String packageName) {
274         try {
275             ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
276                     packageName,
277                     0 /* flags */);
278             return appInfo.loadLabel(context.getPackageManager()).toString();
279         } catch (PackageManager.NameNotFoundException e) {
280             return "";
281         }
282     }
283 
getConnectedDescription(Context context, WifiConfiguration wifiConfiguration, NetworkCapabilities networkCapabilities, String recommendationServiceLabel, boolean isDefaultNetwork, boolean isLowQuality)284     static String getConnectedDescription(Context context,
285             WifiConfiguration wifiConfiguration,
286             NetworkCapabilities networkCapabilities,
287             String recommendationServiceLabel,
288             boolean isDefaultNetwork,
289             boolean isLowQuality) {
290         final StringJoiner sj = new StringJoiner(context.getString(
291                 R.string.wifitrackerlib_summary_separator));
292 
293         if (wifiConfiguration != null) {
294             if (wifiConfiguration.fromWifiNetworkSuggestion
295                     || wifiConfiguration.fromWifiNetworkSpecifier) {
296                 // For suggestion or specifier networks to show "Connected via ..."
297                 final String suggestionOrSpecifierLabel =
298                         getSuggestionOrSpecifierLabel(context, wifiConfiguration);
299                 if (!TextUtils.isEmpty(suggestionOrSpecifierLabel)) {
300                     if (!isDefaultNetwork) {
301                         sj.add(context.getString(R.string.wifitrackerlib_available_via_app,
302                                 suggestionOrSpecifierLabel));
303                     } else {
304                         sj.add(context.getString(R.string.wifitrackerlib_connected_via_app,
305                                 suggestionOrSpecifierLabel));
306                     }
307                 }
308             } else if (wifiConfiguration.isEphemeral() && isDefaultNetwork) {
309                 // For ephemeral networks to show "Automatically connected via ..."
310                 if (!TextUtils.isEmpty(recommendationServiceLabel)) {
311                     sj.add(String.format(context.getString(
312                             R.string.wifitrackerlib_connected_via_network_scorer),
313                             recommendationServiceLabel));
314                 } else {
315                     sj.add(context.getString(
316                             R.string.wifitrackerlib_connected_via_network_scorer_default));
317                 }
318             }
319         }
320 
321         if (isLowQuality) {
322             sj.add(context.getString(R.string.wifi_connected_low_quality));
323         }
324 
325         // For displaying network capability info, such as captive portal or no internet
326         String networkCapabilitiesInformation =
327                 getCurrentNetworkCapabilitiesInformation(context,  networkCapabilities);
328         if (!TextUtils.isEmpty(networkCapabilitiesInformation)) {
329             sj.add(networkCapabilitiesInformation);
330         }
331 
332         // Default to "Connected" if nothing else to display
333         if (sj.length() == 0 && isDefaultNetwork) {
334             return context.getResources().getStringArray(R.array.wifitrackerlib_wifi_status)
335                     [DetailedState.CONNECTED.ordinal()];
336         }
337 
338         return sj.toString();
339     }
340 
getConnectingDescription(Context context, NetworkInfo networkInfo)341     static String getConnectingDescription(Context context, NetworkInfo networkInfo) {
342         if (context == null || networkInfo == null) {
343             return "";
344         }
345         DetailedState detailedState = networkInfo.getDetailedState();
346         if (detailedState == null) {
347             return "";
348         }
349 
350         final String[] wifiStatusArray = context.getResources()
351                 .getStringArray(R.array.wifitrackerlib_wifi_status);
352         final int index = detailedState.ordinal();
353         return index >= wifiStatusArray.length ? "" : wifiStatusArray[index];
354     }
355 
356 
getDisconnectedDescription( @onNull WifiTrackerInjector injector, Context context, WifiConfiguration wifiConfiguration, boolean forSavedNetworksPage, boolean concise)357     static String getDisconnectedDescription(
358             @NonNull WifiTrackerInjector injector,
359             Context context,
360             WifiConfiguration wifiConfiguration,
361             boolean forSavedNetworksPage,
362             boolean concise) {
363         if (context == null || wifiConfiguration == null) {
364             return "";
365         }
366         final StringJoiner sj = new StringJoiner(context.getString(
367                 R.string.wifitrackerlib_summary_separator));
368 
369         // For "Saved", "Saved by ...", and "Available via..."
370         if (concise) {
371             sj.add(context.getString(R.string.wifitrackerlib_wifi_disconnected));
372         } else if (forSavedNetworksPage && !wifiConfiguration.isPasspoint()) {
373             if (!injector.getNoAttributionAnnotationPackages().contains(
374                     wifiConfiguration.creatorName)) {
375                 final CharSequence appLabel = getAppLabel(context,
376                         wifiConfiguration.creatorName);
377                 if (!TextUtils.isEmpty(appLabel)) {
378                     sj.add(context.getString(R.string.wifitrackerlib_saved_network, appLabel));
379                 }
380             }
381         } else {
382             if (wifiConfiguration.fromWifiNetworkSuggestion) {
383                 final String suggestionOrSpecifierLabel =
384                         getSuggestionOrSpecifierLabel(context, wifiConfiguration);
385                 if (!TextUtils.isEmpty(suggestionOrSpecifierLabel)) {
386                     sj.add(context.getString(
387                             R.string.wifitrackerlib_available_via_app,
388                             suggestionOrSpecifierLabel));
389                 }
390             } else {
391                 sj.add(context.getString(R.string.wifitrackerlib_wifi_remembered));
392             }
393         }
394 
395         // For failure messages and disabled reasons
396         final String wifiConfigFailureMessage =
397                 getWifiConfigurationFailureMessage(context, wifiConfiguration);
398         if (!TextUtils.isEmpty(wifiConfigFailureMessage)) {
399             sj.add(wifiConfigFailureMessage);
400         }
401 
402         return sj.toString();
403     }
404 
getSuggestionOrSpecifierLabel( Context context, WifiConfiguration wifiConfiguration)405     private static String getSuggestionOrSpecifierLabel(
406             Context context, WifiConfiguration wifiConfiguration) {
407         if (context == null || wifiConfiguration == null) {
408             return "";
409         }
410 
411         final String carrierName = getCarrierNameForSubId(context,
412                 getSubIdForConfig(context, wifiConfiguration));
413         if (!TextUtils.isEmpty(carrierName)) {
414             return carrierName;
415         }
416         final String suggestorLabel = getAppLabel(context, wifiConfiguration.creatorName);
417         if (!TextUtils.isEmpty(suggestorLabel)) {
418             return suggestorLabel;
419         }
420         // Fall-back to the package name in case the app label is missing
421         return wifiConfiguration.creatorName;
422     }
423 
getWifiConfigurationFailureMessage( Context context, WifiConfiguration wifiConfiguration)424     private static String getWifiConfigurationFailureMessage(
425             Context context, WifiConfiguration wifiConfiguration) {
426         if (context == null || wifiConfiguration == null) {
427             return "";
428         }
429 
430         // Check for any failure messages to display
431         if (wifiConfiguration.hasNoInternetAccess()) {
432             int messageID =
433                     wifiConfiguration.getNetworkSelectionStatus().getNetworkSelectionStatus()
434                             == NETWORK_SELECTION_PERMANENTLY_DISABLED
435                             ? R.string.wifitrackerlib_wifi_no_internet_no_reconnect
436                             : R.string.wifitrackerlib_wifi_no_internet;
437             return context.getString(messageID);
438         } else if (wifiConfiguration.getNetworkSelectionStatus().getNetworkSelectionStatus()
439                 != NETWORK_SELECTION_ENABLED) {
440             WifiConfiguration.NetworkSelectionStatus networkStatus =
441                     wifiConfiguration.getNetworkSelectionStatus();
442             switch (networkStatus.getNetworkSelectionDisableReason()) {
443                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE:
444                 case WifiConfiguration.NetworkSelectionStatus
445                         .DISABLED_AUTHENTICATION_NO_SUBSCRIPTION:
446                     return context.getString(
447                             R.string.wifitrackerlib_wifi_disabled_password_failure);
448                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD:
449                     return context.getString(R.string.wifitrackerlib_wifi_check_password_try_again);
450                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE:
451                     return context.getString(R.string.wifitrackerlib_wifi_disabled_network_failure);
452                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION:
453                     return context.getString(R.string.wifitrackerlib_wifi_disabled_generic);
454                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_NO_INTERNET_PERMANENT:
455                 case WifiConfiguration.NetworkSelectionStatus.DISABLED_NO_INTERNET_TEMPORARY:
456                     return context.getString(R.string.wifitrackerlib_wifi_no_internet_no_reconnect);
457                 default:
458                     break;
459             }
460         } else { // In range, not disabled.
461             switch (wifiConfiguration.getRecentFailureReason()) {
462                 case WifiConfiguration.RECENT_FAILURE_AP_UNABLE_TO_HANDLE_NEW_STA:
463                 case WifiConfiguration.RECENT_FAILURE_REFUSED_TEMPORARILY:
464                 case WifiConfiguration.RECENT_FAILURE_DISCONNECTION_AP_BUSY:
465                     return context.getString(R.string
466                             .wifitrackerlib_wifi_ap_unable_to_handle_new_sta);
467                 case WifiConfiguration.RECENT_FAILURE_POOR_CHANNEL_CONDITIONS:
468                     return context.getString(R.string.wifitrackerlib_wifi_poor_channel_conditions);
469                 case WifiConfiguration.RECENT_FAILURE_MBO_ASSOC_DISALLOWED_UNSPECIFIED:
470                 case WifiConfiguration.RECENT_FAILURE_MBO_ASSOC_DISALLOWED_AIR_INTERFACE_OVERLOADED:
471                 case WifiConfiguration.RECENT_FAILURE_MBO_ASSOC_DISALLOWED_AUTH_SERVER_OVERLOADED:
472                     return context.getString(R.string
473                             .wifitrackerlib_wifi_mbo_assoc_disallowed_cannot_connect);
474                 case WifiConfiguration.RECENT_FAILURE_MBO_ASSOC_DISALLOWED_MAX_NUM_STA_ASSOCIATED:
475                     return context.getString(R.string
476                             .wifitrackerlib_wifi_mbo_assoc_disallowed_max_num_sta_associated);
477                 case WifiConfiguration.RECENT_FAILURE_MBO_ASSOC_DISALLOWED_INSUFFICIENT_RSSI:
478                 case WifiConfiguration.RECENT_FAILURE_OCE_RSSI_BASED_ASSOCIATION_REJECTION:
479                     return context.getString(R.string
480                             .wifitrackerlib_wifi_mbo_oce_assoc_disallowed_insufficient_rssi);
481                 case WifiConfiguration.RECENT_FAILURE_NETWORK_NOT_FOUND:
482                     return context.getString(R.string.wifitrackerlib_wifi_network_not_found);
483                 default:
484                     // do nothing
485             }
486         }
487         return "";
488     }
489 
getAutoConnectDescription(@onNull Context context, @NonNull WifiEntry wifiEntry)490     static String getAutoConnectDescription(@NonNull Context context,
491             @NonNull WifiEntry wifiEntry) {
492         if (context == null || wifiEntry == null || !wifiEntry.canSetAutoJoinEnabled()) {
493             return "";
494         }
495 
496         return wifiEntry.isAutoJoinEnabled()
497                 ? "" : context.getString(R.string.wifitrackerlib_auto_connect_disable);
498     }
499 
getMeteredDescription(@onNull Context context, @Nullable WifiEntry wifiEntry)500     static String getMeteredDescription(@NonNull Context context, @Nullable WifiEntry wifiEntry) {
501         if (context == null || wifiEntry == null) {
502             return "";
503         }
504 
505         if (!wifiEntry.canSetMeteredChoice()
506                 && wifiEntry.getMeteredChoice() != WifiEntry.METERED_CHOICE_METERED) {
507             return "";
508         }
509 
510         if (wifiEntry.getMeteredChoice() == WifiEntry.METERED_CHOICE_METERED) {
511             return context.getString(R.string.wifitrackerlib_wifi_metered_label);
512         } else if (wifiEntry.getMeteredChoice() == WifiEntry.METERED_CHOICE_UNMETERED) {
513             return context.getString(R.string.wifitrackerlib_wifi_unmetered_label);
514         } else { // METERED_CHOICE_AUTO
515             return wifiEntry.isMetered() ? context.getString(
516                     R.string.wifitrackerlib_wifi_metered_label) : "";
517         }
518     }
519 
getSpeedDescription(@onNull Context context, @NonNull WifiEntry wifiEntry)520     static String getSpeedDescription(@NonNull Context context, @NonNull WifiEntry wifiEntry) {
521         if (context == null || wifiEntry == null) {
522             return "";
523         }
524 
525         @Speed int speed = wifiEntry.getSpeed();
526         switch (speed) {
527             case SPEED_VERY_FAST:
528                 return context.getString(R.string.wifitrackerlib_speed_label_very_fast);
529             case SPEED_FAST:
530                 return context.getString(R.string.wifitrackerlib_speed_label_fast);
531             case SPEED_MODERATE:
532                 return context.getString(R.string.wifitrackerlib_speed_label_okay);
533             case SPEED_SLOW:
534                 return context.getString(R.string.wifitrackerlib_speed_label_slow);
535             case SPEED_NONE:
536             default:
537                 return "";
538         }
539     }
540 
getVerboseLoggingDescription(@onNull WifiEntry wifiEntry)541     static String getVerboseLoggingDescription(@NonNull WifiEntry wifiEntry) {
542         if (!BaseWifiTracker.isVerboseLoggingEnabled() || wifiEntry == null) {
543             return "";
544         }
545 
546         final StringJoiner sj = new StringJoiner(" ");
547 
548         final String wifiInfoDescription = wifiEntry.getWifiInfoDescription();
549         if (!TextUtils.isEmpty(wifiInfoDescription)) {
550             sj.add(wifiInfoDescription);
551         }
552 
553         final String networkCapabilityDescription = wifiEntry.getNetworkCapabilityDescription();
554         if (!TextUtils.isEmpty(networkCapabilityDescription)) {
555             sj.add(networkCapabilityDescription);
556         }
557 
558         final String scanResultsDescription = wifiEntry.getScanResultDescription();
559         if (!TextUtils.isEmpty(scanResultsDescription)) {
560             sj.add(scanResultsDescription);
561         }
562 
563         final String networkSelectionDescription = wifiEntry.getNetworkSelectionDescription();
564         if (!TextUtils.isEmpty(networkSelectionDescription)) {
565             sj.add(networkSelectionDescription);
566         }
567 
568         return sj.toString();
569     }
570 
getNetworkSelectionDescription(WifiConfiguration wifiConfig)571     static String getNetworkSelectionDescription(WifiConfiguration wifiConfig) {
572         if (wifiConfig == null) {
573             return "";
574         }
575 
576         StringBuilder description = new StringBuilder();
577         NetworkSelectionStatus networkSelectionStatus = wifiConfig.getNetworkSelectionStatus();
578 
579         if (networkSelectionStatus.getNetworkSelectionStatus() != NETWORK_SELECTION_ENABLED) {
580             description.append(" (" + networkSelectionStatus.getNetworkStatusString());
581             if (networkSelectionStatus.getDisableTime() > 0) {
582                 long now = System.currentTimeMillis();
583                 long elapsedSeconds = (now - networkSelectionStatus.getDisableTime()) / 1000;
584                 description.append(" " + DateUtils.formatElapsedTime(elapsedSeconds));
585             }
586             description.append(")");
587         }
588 
589         int maxNetworkSelectionDisableReason =
590                 NetworkSelectionStatus.getMaxNetworkSelectionDisableReason();
591         for (int reason = 0; reason <= maxNetworkSelectionDisableReason; reason++) {
592             int disableReasonCounter = networkSelectionStatus.getDisableReasonCounter(reason);
593             if (disableReasonCounter == 0) {
594                 continue;
595             }
596             description.append(" ")
597                     .append(NetworkSelectionStatus.getNetworkSelectionDisableReasonString(reason))
598                     .append("=")
599                     .append(disableReasonCounter);
600         }
601         return description.toString();
602     }
603 
getCurrentNetworkCapabilitiesInformation(Context context, NetworkCapabilities networkCapabilities)604     static String getCurrentNetworkCapabilitiesInformation(Context context,
605             NetworkCapabilities networkCapabilities) {
606         if (context == null || networkCapabilities == null) {
607             return "";
608         }
609 
610         if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)) {
611             return context.getString(context.getResources()
612                     .getIdentifier("network_available_sign_in", "string", "android"));
613         }
614 
615         if (networkCapabilities.hasCapability(
616                 NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY)) {
617             return context.getString(R.string.wifitrackerlib_wifi_limited_connection);
618         }
619 
620         if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
621             if (networkCapabilities.isPrivateDnsBroken()) {
622                 return context.getString(R.string.wifitrackerlib_private_dns_broken);
623             }
624             return context.getString(
625                 R.string.wifitrackerlib_wifi_connected_cannot_provide_internet);
626         }
627         return "";
628     }
629 
630     /**
631      * Returns the display string corresponding to the detailed state of the given NetworkInfo
632      */
getNetworkDetailedState(Context context, NetworkInfo networkInfo)633     static String getNetworkDetailedState(Context context, NetworkInfo networkInfo) {
634         if (context == null || networkInfo == null) {
635             return "";
636         }
637         DetailedState detailedState = networkInfo.getDetailedState();
638         if (detailedState == null) {
639             return "";
640         }
641 
642         String[] wifiStatusArray = context.getResources()
643                 .getStringArray(R.array.wifitrackerlib_wifi_status);
644         int index = detailedState.ordinal();
645         return index >= wifiStatusArray.length ? "" : wifiStatusArray[index];
646     }
647 
648     /**
649      * Check if the SIM is present for target carrier Id. If the carrierId is
650      * {@link TelephonyManager#UNKNOWN_CARRIER_ID}, then this returns true if there is any SIM
651      * present.
652      */
isSimPresent(@onNull Context context, int carrierId)653     static boolean isSimPresent(@NonNull Context context, int carrierId) {
654         SubscriptionManager subscriptionManager =
655                 (SubscriptionManager) context.getSystemService(
656                         Context.TELEPHONY_SUBSCRIPTION_SERVICE);
657         if (subscriptionManager == null) return false;
658         List<SubscriptionInfo> subInfoList = subscriptionManager.getActiveSubscriptionInfoList();
659         if (subInfoList == null || subInfoList.isEmpty()) {
660             return false;
661         }
662         if (carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
663             // Return true if any SIM is present for UNKNOWN_CARRIER_ID since the framework will
664             // match this to the default data SIM.
665             return true;
666         }
667         return subInfoList.stream()
668                 .anyMatch(info -> info.getCarrierId() == carrierId);
669     }
670 
671     /**
672      * Get the SIM carrier name for target subscription Id.
673      */
getCarrierNameForSubId(@onNull Context context, int subId)674     static @Nullable String getCarrierNameForSubId(@NonNull Context context, int subId) {
675         if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
676             return null;
677         }
678         TelephonyManager telephonyManager =
679                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
680         if (telephonyManager == null) return null;
681         TelephonyManager specifiedTm = telephonyManager.createForSubscriptionId(subId);
682         if (specifiedTm == null) {
683             return null;
684         }
685         CharSequence name = specifiedTm.getSimCarrierIdName();
686         if (name == null) {
687             return null;
688         }
689         return name.toString();
690     }
691 
isSimCredential(@onNull WifiConfiguration config)692     static boolean isSimCredential(@NonNull WifiConfiguration config) {
693         return config.enterpriseConfig != null
694                 && config.enterpriseConfig.isAuthenticationSimBased();
695     }
696 
697     /**
698      * Get the best match subscription Id for target WifiConfiguration.
699      */
getSubIdForConfig(@onNull Context context, @NonNull WifiConfiguration config)700     static int getSubIdForConfig(@NonNull Context context, @NonNull WifiConfiguration config) {
701         if (config.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
702             return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
703         }
704         SubscriptionManager subscriptionManager =
705                 (SubscriptionManager) context.getSystemService(
706                         Context.TELEPHONY_SUBSCRIPTION_SERVICE);
707         if (subscriptionManager == null) {
708             return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
709         }
710         List<SubscriptionInfo> subInfoList = subscriptionManager.getActiveSubscriptionInfoList();
711         if (subInfoList == null || subInfoList.isEmpty()) {
712             return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
713         }
714 
715         int matchSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
716         int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId();
717         for (SubscriptionInfo subInfo : subInfoList) {
718             if (subInfo.getCarrierId() == config.carrierId) {
719                 matchSubId = subInfo.getSubscriptionId();
720                 if (matchSubId == dataSubId) {
721                     // Priority of Data sub is higher than non data sub.
722                     break;
723                 }
724             }
725         }
726         return matchSubId;
727     }
728 
729     /**
730      * Check if target subscription Id requires IMSI privacy protection.
731      */
isImsiPrivacyProtectionProvided(@onNull Context context, int subId)732     static boolean isImsiPrivacyProtectionProvided(@NonNull Context context, int subId) {
733         CarrierConfigManager carrierConfigManager =
734                 (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
735         if (carrierConfigManager == null) {
736             return false;
737         }
738         PersistableBundle bundle = carrierConfigManager.getConfigForSubId(subId);
739         if (bundle == null) {
740             return false;
741         }
742         return (bundle.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT)
743                 & TelephonyManager.KEY_TYPE_WLAN) != 0;
744     }
745 
getImsiProtectionDescription(Context context, @Nullable WifiConfiguration wifiConfig)746     static CharSequence getImsiProtectionDescription(Context context,
747             @Nullable WifiConfiguration wifiConfig) {
748         if (context == null || wifiConfig == null || !isSimCredential(wifiConfig)) {
749             return "";
750         }
751         int subId;
752         if (wifiConfig.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
753             // Config without carrierId use default data subscription.
754             subId = SubscriptionManager.getDefaultSubscriptionId();
755         } else {
756             subId = getSubIdForConfig(context, wifiConfig);
757         }
758         if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
759                 || isImsiPrivacyProtectionProvided(context, subId)) {
760             return "";
761         }
762 
763         // IMSI protection is not provided, return warning message.
764         return linkifyAnnotation(context, context.getText(
765                 R.string.wifitrackerlib_imsi_protection_warning), "url",
766                 context.getString(R.string.wifitrackerlib_help_url_imsi_protection));
767     }
768 
769     /** Find the annotation of specified id in rawText and linkify it with helpUriString. */
linkifyAnnotation(Context context, CharSequence rawText, String id, String helpUriString)770     static CharSequence linkifyAnnotation(Context context, CharSequence rawText, String id,
771             String helpUriString) {
772         // Return original string when helpUriString is empty.
773         if (TextUtils.isEmpty(helpUriString)) {
774             return rawText;
775         }
776 
777         SpannableString spannableText = new SpannableString(rawText);
778         Annotation[] annotations = spannableText.getSpans(0, spannableText.length(),
779                 Annotation.class);
780 
781         for (Annotation annotation : annotations) {
782             if (TextUtils.equals(annotation.getValue(), id)) {
783                 SpannableStringBuilder builder = new SpannableStringBuilder(spannableText);
784                 ClickableSpan link = new ClickableSpan() {
785                     @Override
786                     public void onClick(View view) {
787                         view.startActivityForResult(HelpUtils.getHelpIntent(context, helpUriString,
788                                 view.getClass().getName()), 0);
789                     }
790                 };
791                 builder.setSpan(link, spannableText.getSpanStart(annotation),
792                         spannableText.getSpanEnd(annotation), spannableText.getSpanFlags(link));
793                 return builder;
794             }
795         }
796         return rawText;
797     }
798 
799     // Various utility methods copied from com.android.server.wifi.util.ScanResultUtils for
800     // extracting SecurityType from ScanResult.
801 
802     /**
803      * Helper method to check if the provided |scanResult| corresponds to a PSK network or not.
804      * This checks if the provided capabilities string contains PSK encryption type or not.
805      * TODO(b/187755981): Move to shared static utils class
806      */
isScanResultForPskNetwork(ScanResult scanResult)807     public static boolean isScanResultForPskNetwork(ScanResult scanResult) {
808         return scanResult.capabilities.contains("PSK");
809     }
810 
811     /**
812      * Helper method to check if the provided |scanResult| corresponds to a WAPI-PSK network or not.
813      * This checks if the provided capabilities string contains PSK encryption type or not.
814      * TODO(b/187755981): Move to shared static utils class
815      */
isScanResultForWapiPskNetwork(ScanResult scanResult)816     public static boolean isScanResultForWapiPskNetwork(ScanResult scanResult) {
817         return scanResult.capabilities.contains("WAPI-PSK");
818     }
819 
820     /**
821      * Helper method to check if the provided |scanResult| corresponds to a WAPI-CERT
822      * network or not.
823      * This checks if the provided capabilities string contains PSK encryption type or not.
824      * TODO(b/187755981): Move to shared static utils class
825      */
isScanResultForWapiCertNetwork(ScanResult scanResult)826     public static boolean isScanResultForWapiCertNetwork(ScanResult scanResult) {
827         return scanResult.capabilities.contains("WAPI-CERT");
828     }
829 
830     /**
831      * Helper method to check if the provided |scanResult| corresponds to a EAP network or not.
832      * This checks these conditions:
833      * - Enable EAP/SHA1, EAP/SHA256 AKM, FT/EAP, or EAP-FILS.
834      * - Not a WPA3 Enterprise only network.
835      * - Not a WPA3 Enterprise transition network.
836      * TODO(b/187755981): Move to shared static utils class
837      */
isScanResultForEapNetwork(ScanResult scanResult)838     public static boolean isScanResultForEapNetwork(ScanResult scanResult) {
839         return (scanResult.capabilities.contains("EAP/SHA1")
840                 || scanResult.capabilities.contains("EAP/SHA256")
841                 || scanResult.capabilities.contains("FT/EAP")
842                 || scanResult.capabilities.contains("EAP-FILS"))
843                 && !isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)
844                 && !isScanResultForWpa3EnterpriseTransitionNetwork(scanResult);
845     }
846 
847     // TODO(b/187755981): Move to shared static utils class
isScanResultForPmfMandatoryNetwork(ScanResult scanResult)848     private static boolean isScanResultForPmfMandatoryNetwork(ScanResult scanResult) {
849         return scanResult.capabilities.contains("[MFPR]");
850     }
851 
852     // TODO(b/187755981): Move to shared static utils class
isScanResultForPmfCapableNetwork(ScanResult scanResult)853     private static boolean isScanResultForPmfCapableNetwork(ScanResult scanResult) {
854         return scanResult.capabilities.contains("[MFPC]");
855     }
856 
857     /**
858      * Helper method to check if the provided |scanResult| corresponds to
859      * a WPA3 Enterprise transition network or not.
860      *
861      * See Section 3.3 WPA3-Enterprise transition mode in WPA3 Specification
862      * - Enable at least EAP/SHA1 and EAP/SHA256 AKM suites.
863      * - Not enable WPA1 version 1, WEP, and TKIP.
864      * - Management Frame Protection Capable is set.
865      * - Management Frame Protection Required is not set.
866      * TODO(b/187755981): Move to shared static utils class
867      */
isScanResultForWpa3EnterpriseTransitionNetwork(ScanResult scanResult)868     public static boolean isScanResultForWpa3EnterpriseTransitionNetwork(ScanResult scanResult) {
869         return scanResult.capabilities.contains("EAP/SHA1")
870                 && scanResult.capabilities.contains("EAP/SHA256")
871                 && scanResult.capabilities.contains("RSN")
872                 && !scanResult.capabilities.contains("WEP")
873                 && !scanResult.capabilities.contains("TKIP")
874                 && !isScanResultForPmfMandatoryNetwork(scanResult)
875                 && isScanResultForPmfCapableNetwork(scanResult);
876     }
877 
878     /**
879      * Helper method to check if the provided |scanResult| corresponds to
880      * a WPA3 Enterprise only network or not.
881      *
882      * See Section 3.2 WPA3-Enterprise only mode in WPA3 Specification
883      * - Enable at least EAP/SHA256 AKM suite.
884      * - Not enable EAP/SHA1 AKM suite.
885      * - Not enable WPA1 version 1, WEP, and TKIP.
886      * - Management Frame Protection Capable is set.
887      * - Management Frame Protection Required is set.
888      * TODO(b/187755981): Move to shared static utils class
889      */
isScanResultForWpa3EnterpriseOnlyNetwork(ScanResult scanResult)890     public static boolean isScanResultForWpa3EnterpriseOnlyNetwork(ScanResult scanResult) {
891         return scanResult.capabilities.contains("EAP/SHA256")
892                 && !scanResult.capabilities.contains("EAP/SHA1")
893                 && scanResult.capabilities.contains("RSN")
894                 && !scanResult.capabilities.contains("WEP")
895                 && !scanResult.capabilities.contains("TKIP")
896                 && isScanResultForPmfMandatoryNetwork(scanResult)
897                 && isScanResultForPmfCapableNetwork(scanResult);
898     }
899 
900 
901     /**
902      * Helper method to check if the provided |scanResult| corresponds to a WPA3-Enterprise 192-bit
903      * mode network or not.
904      * This checks if the provided capabilities comply these conditions:
905      * - Enable SUITE-B-192 AKM.
906      * - Not enable EAP/SHA1 AKM suite.
907      * - Not enable WPA1 version 1, WEP, and TKIP.
908      * - Management Frame Protection Required is set.
909      * TODO(b/187755981): Move to shared static utils class
910      */
isScanResultForEapSuiteBNetwork(ScanResult scanResult)911     public static boolean isScanResultForEapSuiteBNetwork(ScanResult scanResult) {
912         return scanResult.capabilities.contains("SUITE_B_192")
913                 && scanResult.capabilities.contains("RSN")
914                 && !scanResult.capabilities.contains("WEP")
915                 && !scanResult.capabilities.contains("TKIP")
916                 && isScanResultForPmfMandatoryNetwork(scanResult);
917     }
918 
919     /**
920      * Helper method to check if the provided |scanResult| corresponds to a WEP network or not.
921      * This checks if the provided capabilities string contains WEP encryption type or not.
922      * TODO(b/187755981): Move to shared static utils class
923      */
isScanResultForWepNetwork(ScanResult scanResult)924     public static boolean isScanResultForWepNetwork(ScanResult scanResult) {
925         return scanResult.capabilities.contains("WEP");
926     }
927 
928     /**
929      * Helper method to check if the provided |scanResult| corresponds to OWE network.
930      * This checks if the provided capabilities string contains OWE or not.
931      * TODO(b/187755981): Move to shared static utils class
932      */
isScanResultForOweNetwork(ScanResult scanResult)933     public static boolean isScanResultForOweNetwork(ScanResult scanResult) {
934         return scanResult.capabilities.contains("OWE");
935     }
936 
937     /**
938      * Helper method to check if the provided |scanResult| corresponds to OWE transition network.
939      * This checks if the provided capabilities string contains OWE_TRANSITION or not.
940      * TODO(b/187755981): Move to shared static utils class
941      */
isScanResultForOweTransitionNetwork(ScanResult scanResult)942     public static boolean isScanResultForOweTransitionNetwork(ScanResult scanResult) {
943         return scanResult.capabilities.contains("OWE_TRANSITION");
944     }
945 
946     /**
947      * Helper method to check if the provided |scanResult| corresponds to SAE network.
948      * This checks if the provided capabilities string contains SAE or not.
949      * TODO(b/187755981): Move to shared static utils class
950      */
isScanResultForSaeNetwork(ScanResult scanResult)951     public static boolean isScanResultForSaeNetwork(ScanResult scanResult) {
952         return scanResult.capabilities.contains("SAE");
953     }
954 
955     /**
956      * Helper method to check if the provided |scanResult| corresponds to PSK-SAE transition
957      * network. This checks if the provided capabilities string contains both PSK and SAE or not.
958      * TODO(b/187755981): Move to shared static utils class
959      */
isScanResultForPskSaeTransitionNetwork(ScanResult scanResult)960     public static boolean isScanResultForPskSaeTransitionNetwork(ScanResult scanResult) {
961         return scanResult.capabilities.contains("PSK") && scanResult.capabilities.contains("SAE");
962     }
963 
964     /**
965      *  Helper method to check if the provided |scanResult| corresponds to an unknown amk network.
966      *  This checks if the provided capabilities string contains ? or not.
967      *  TODO(b/187755981): Move to shared static utils class
968      */
isScanResultForUnknownAkmNetwork(ScanResult scanResult)969     public static boolean isScanResultForUnknownAkmNetwork(ScanResult scanResult) {
970         return scanResult.capabilities.contains("?");
971     }
972 
973     /**
974      * Helper method to check if the provided |scanResult| corresponds to an open network or not.
975      * This checks if the provided capabilities string does not contain either of WEP, PSK, SAE
976      * EAP, or unknown encryption types or not.
977      * TODO(b/187755981): Move to shared static utils class
978      */
isScanResultForOpenNetwork(ScanResult scanResult)979     public static boolean isScanResultForOpenNetwork(ScanResult scanResult) {
980         return (!(isScanResultForWepNetwork(scanResult) || isScanResultForPskNetwork(scanResult)
981                 || isScanResultForEapNetwork(scanResult) || isScanResultForSaeNetwork(scanResult)
982                 || isScanResultForWpa3EnterpriseTransitionNetwork(scanResult)
983                 || isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)
984                 || isScanResultForWapiPskNetwork(scanResult)
985                 || isScanResultForWapiCertNetwork(scanResult)
986                 || isScanResultForEapSuiteBNetwork(scanResult)
987                 || isScanResultForUnknownAkmNetwork(scanResult)));
988     }
989 }
990