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