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.DISABLED_AUTHENTICATION_FAILURE; 20 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLED_AUTHENTICATION_NO_CREDENTIALS; 21 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD; 22 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; 23 import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; 24 import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP; 25 import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE; 26 import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT; 27 import static android.net.wifi.WifiInfo.SECURITY_TYPE_OPEN; 28 import static android.net.wifi.WifiInfo.SECURITY_TYPE_OWE; 29 import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK; 30 import static android.net.wifi.WifiInfo.SECURITY_TYPE_SAE; 31 import static android.net.wifi.WifiInfo.SECURITY_TYPE_UNKNOWN; 32 import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP; 33 import static android.net.wifi.WifiInfo.sanitizeSsid; 34 35 import static com.android.wifitrackerlib.Utils.getAutoConnectDescription; 36 import static com.android.wifitrackerlib.Utils.getAverageSpeedFromScanResults; 37 import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel; 38 import static com.android.wifitrackerlib.Utils.getConnectedDescription; 39 import static com.android.wifitrackerlib.Utils.getConnectingDescription; 40 import static com.android.wifitrackerlib.Utils.getDisconnectedDescription; 41 import static com.android.wifitrackerlib.Utils.getImsiProtectionDescription; 42 import static com.android.wifitrackerlib.Utils.getMeteredDescription; 43 import static com.android.wifitrackerlib.Utils.getSecurityTypesFromScanResult; 44 import static com.android.wifitrackerlib.Utils.getSecurityTypesFromWifiConfiguration; 45 import static com.android.wifitrackerlib.Utils.getSingleSecurityTypeFromMultipleSecurityTypes; 46 import static com.android.wifitrackerlib.Utils.getSpeedDescription; 47 import static com.android.wifitrackerlib.Utils.getSpeedFromWifiInfo; 48 import static com.android.wifitrackerlib.Utils.getVerboseLoggingDescription; 49 50 import android.content.Context; 51 import android.net.ConnectivityManager; 52 import android.net.NetworkCapabilities; 53 import android.net.NetworkInfo; 54 import android.net.NetworkScoreManager; 55 import android.net.NetworkScorerAppData; 56 import android.net.wifi.ScanResult; 57 import android.net.wifi.WifiConfiguration; 58 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus; 59 import android.net.wifi.WifiInfo; 60 import android.net.wifi.WifiManager; 61 import android.net.wifi.WifiNetworkScoreCache; 62 import android.os.Handler; 63 import android.os.SystemClock; 64 import android.telephony.SubscriptionInfo; 65 import android.telephony.SubscriptionManager; 66 import android.telephony.TelephonyManager; 67 import android.text.TextUtils; 68 import android.util.ArraySet; 69 import android.util.Log; 70 71 import androidx.annotation.NonNull; 72 import androidx.annotation.Nullable; 73 import androidx.annotation.WorkerThread; 74 75 import com.android.internal.annotations.VisibleForTesting; 76 77 import org.json.JSONArray; 78 import org.json.JSONException; 79 import org.json.JSONObject; 80 81 import java.util.ArrayList; 82 import java.util.Collections; 83 import java.util.Comparator; 84 import java.util.HashMap; 85 import java.util.List; 86 import java.util.Map; 87 import java.util.Objects; 88 import java.util.Set; 89 import java.util.StringJoiner; 90 import java.util.stream.Collectors; 91 92 /** 93 * WifiEntry representation of a logical Wi-Fi network, uniquely identified by SSID and security. 94 * 95 * This type of WifiEntry can represent both open and saved networks. 96 */ 97 @VisibleForTesting 98 public class StandardWifiEntry extends WifiEntry { 99 static final String TAG = "StandardWifiEntry"; 100 public static final String KEY_PREFIX = "StandardWifiEntry:"; 101 102 @NonNull private final StandardWifiEntryKey mKey; 103 104 @NonNull private final WifiTrackerInjector mInjector; 105 @NonNull private final Context mContext; 106 107 // Map of security type to matching scan results 108 @NonNull private final Map<Integer, List<ScanResult>> mMatchingScanResults = new HashMap<>(); 109 // Map of security type to matching WifiConfiguration 110 // TODO: Change this to single WifiConfiguration once we can get multiple security type configs. 111 @NonNull private final Map<Integer, WifiConfiguration> mMatchingWifiConfigs = new HashMap<>(); 112 113 // List of the target scan results to be displayed. This should match the highest available 114 // security from all of the matched WifiConfigurations. 115 // If no WifiConfigurations are available, then these should match the most appropriate security 116 // type (e.g. PSK for an PSK/SAE entry, OWE for an Open/OWE entry). 117 @NonNull private final List<ScanResult> mTargetScanResults = new ArrayList<>(); 118 // Target WifiConfiguration for connection and displaying WifiConfiguration info 119 private WifiConfiguration mTargetWifiConfig; 120 private List<Integer> mTargetSecurityTypes = new ArrayList<>(); 121 122 private boolean mIsUserShareable = false; 123 @Nullable private String mRecommendationServiceLabel; 124 125 private boolean mShouldAutoOpenCaptivePortal = false; 126 127 private final boolean mIsWpa3SaeSupported; 128 private final boolean mIsWpa3SuiteBSupported; 129 private final boolean mIsEnhancedOpenSupported; 130 StandardWifiEntry( @onNull WifiTrackerInjector injector, @NonNull Context context, @NonNull Handler callbackHandler, @NonNull StandardWifiEntryKey key, @NonNull WifiManager wifiManager, @NonNull WifiNetworkScoreCache scoreCache, boolean forSavedNetworksPage)131 StandardWifiEntry( 132 @NonNull WifiTrackerInjector injector, 133 @NonNull Context context, @NonNull Handler callbackHandler, 134 @NonNull StandardWifiEntryKey key, @NonNull WifiManager wifiManager, 135 @NonNull WifiNetworkScoreCache scoreCache, 136 boolean forSavedNetworksPage) { 137 super(callbackHandler, wifiManager, scoreCache, forSavedNetworksPage); 138 mInjector = injector; 139 mContext = context; 140 mKey = key; 141 mIsWpa3SaeSupported = wifiManager.isWpa3SaeSupported(); 142 mIsWpa3SuiteBSupported = wifiManager.isWpa3SuiteBSupported(); 143 mIsEnhancedOpenSupported = wifiManager.isEnhancedOpenSupported(); 144 updateRecommendationServiceLabel(); 145 } 146 StandardWifiEntry( @onNull WifiTrackerInjector injector, @NonNull Context context, @NonNull Handler callbackHandler, @NonNull StandardWifiEntryKey key, @Nullable List<WifiConfiguration> configs, @Nullable List<ScanResult> scanResults, @NonNull WifiManager wifiManager, @NonNull WifiNetworkScoreCache scoreCache, boolean forSavedNetworksPage)147 StandardWifiEntry( 148 @NonNull WifiTrackerInjector injector, 149 @NonNull Context context, @NonNull Handler callbackHandler, 150 @NonNull StandardWifiEntryKey key, 151 @Nullable List<WifiConfiguration> configs, 152 @Nullable List<ScanResult> scanResults, 153 @NonNull WifiManager wifiManager, 154 @NonNull WifiNetworkScoreCache scoreCache, 155 boolean forSavedNetworksPage) throws IllegalArgumentException { 156 this(injector, context, callbackHandler, key, wifiManager, scoreCache, 157 forSavedNetworksPage); 158 if (configs != null && !configs.isEmpty()) { 159 updateConfig(configs); 160 } 161 if (scanResults != null && !scanResults.isEmpty()) { 162 updateScanResultInfo(scanResults); 163 } 164 } 165 166 @Override getKey()167 public String getKey() { 168 return mKey.toString(); 169 } 170 getStandardWifiEntryKey()171 StandardWifiEntryKey getStandardWifiEntryKey() { 172 return mKey; 173 } 174 175 @Override getTitle()176 public String getTitle() { 177 return mKey.getScanResultKey().getSsid(); 178 } 179 180 @Override getSummary(boolean concise)181 public synchronized String getSummary(boolean concise) { 182 StringJoiner sj = new StringJoiner(mContext.getString( 183 R.string.wifitrackerlib_summary_separator)); 184 185 final String connectedStateDescription; 186 final @ConnectedState int connectedState = getConnectedState(); 187 switch (connectedState) { 188 case CONNECTED_STATE_DISCONNECTED: 189 connectedStateDescription = getDisconnectedDescription(mInjector, mContext, 190 mTargetWifiConfig, 191 mForSavedNetworksPage, 192 concise); 193 break; 194 case CONNECTED_STATE_CONNECTING: 195 connectedStateDescription = getConnectingDescription(mContext, mNetworkInfo); 196 break; 197 case CONNECTED_STATE_CONNECTED: 198 connectedStateDescription = getConnectedDescription(mContext, 199 mTargetWifiConfig, 200 mNetworkCapabilities, 201 mRecommendationServiceLabel, 202 mIsDefaultNetwork, 203 mIsLowQuality); 204 break; 205 default: 206 Log.e(TAG, "getConnectedState() returned unknown state: " + connectedState); 207 connectedStateDescription = null; 208 } 209 if (!TextUtils.isEmpty(connectedStateDescription)) { 210 sj.add(connectedStateDescription); 211 } 212 213 final String speedDescription = getSpeedDescription(mContext, this); 214 if (!TextUtils.isEmpty(speedDescription)) { 215 sj.add(speedDescription); 216 } 217 218 final String autoConnectDescription = getAutoConnectDescription(mContext, this); 219 if (!TextUtils.isEmpty(autoConnectDescription)) { 220 sj.add(autoConnectDescription); 221 } 222 223 final String meteredDescription = getMeteredDescription(mContext, this); 224 if (!TextUtils.isEmpty(meteredDescription)) { 225 sj.add(meteredDescription); 226 } 227 228 if (!concise) { 229 final String verboseLoggingDescription = getVerboseLoggingDescription(this); 230 if (!TextUtils.isEmpty(verboseLoggingDescription)) { 231 sj.add(verboseLoggingDescription); 232 } 233 } 234 235 return sj.toString(); 236 } 237 238 @Override getSecondSummary()239 public CharSequence getSecondSummary() { 240 return getConnectedState() == CONNECTED_STATE_CONNECTED 241 ? getImsiProtectionDescription(mContext, getWifiConfiguration()) : ""; 242 } 243 244 @Override getSsid()245 public String getSsid() { 246 return mKey.getScanResultKey().getSsid(); 247 } 248 249 @Override getSecurityTypes()250 public synchronized List<Integer> getSecurityTypes() { 251 return new ArrayList<>(mTargetSecurityTypes); 252 } 253 254 @Override getMacAddress()255 public synchronized String getMacAddress() { 256 if (mWifiInfo != null) { 257 final String wifiInfoMac = mWifiInfo.getMacAddress(); 258 if (!TextUtils.isEmpty(wifiInfoMac) 259 && !TextUtils.equals(wifiInfoMac, DEFAULT_MAC_ADDRESS)) { 260 return wifiInfoMac; 261 } 262 } 263 if (mTargetWifiConfig == null || getPrivacy() != PRIVACY_RANDOMIZED_MAC) { 264 final String[] factoryMacs = mWifiManager.getFactoryMacAddresses(); 265 if (factoryMacs.length > 0) { 266 return factoryMacs[0]; 267 } 268 return null; 269 } 270 return mTargetWifiConfig.getRandomizedMacAddress().toString(); 271 } 272 273 @Override isMetered()274 public synchronized boolean isMetered() { 275 return getMeteredChoice() == METERED_CHOICE_METERED 276 || (mTargetWifiConfig != null && mTargetWifiConfig.meteredHint); 277 } 278 279 @Override isSaved()280 public synchronized boolean isSaved() { 281 return mTargetWifiConfig != null && !mTargetWifiConfig.fromWifiNetworkSuggestion 282 && !mTargetWifiConfig.isEphemeral(); 283 } 284 285 @Override isSuggestion()286 public synchronized boolean isSuggestion() { 287 return mTargetWifiConfig != null && mTargetWifiConfig.fromWifiNetworkSuggestion; 288 } 289 290 @Override getWifiConfiguration()291 public synchronized WifiConfiguration getWifiConfiguration() { 292 if (!isSaved()) { 293 return null; 294 } 295 return mTargetWifiConfig; 296 } 297 298 @Override canConnect()299 public synchronized boolean canConnect() { 300 if (mLevel == WIFI_LEVEL_UNREACHABLE 301 || getConnectedState() != CONNECTED_STATE_DISCONNECTED) { 302 return false; 303 } 304 // Allow connection for EAP SIM dependent methods if the SIM of specified carrier ID is 305 // active in the device. 306 if (mTargetSecurityTypes.contains(SECURITY_TYPE_EAP) && mTargetWifiConfig != null 307 && mTargetWifiConfig.enterpriseConfig != null) { 308 if (!mTargetWifiConfig.enterpriseConfig.isAuthenticationSimBased()) { 309 return true; 310 } 311 List<SubscriptionInfo> activeSubscriptionInfos = ((SubscriptionManager) mContext 312 .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)) 313 .getActiveSubscriptionInfoList(); 314 if (activeSubscriptionInfos == null || activeSubscriptionInfos.size() == 0) { 315 return false; 316 } 317 if (mTargetWifiConfig.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { 318 // To connect via default subscription. 319 return true; 320 } 321 for (SubscriptionInfo subscriptionInfo : activeSubscriptionInfos) { 322 if (subscriptionInfo.getCarrierId() == mTargetWifiConfig.carrierId) { 323 return true; 324 } 325 } 326 return false; 327 } 328 return true; 329 } 330 331 @Override connect(@ullable ConnectCallback callback)332 public synchronized void connect(@Nullable ConnectCallback callback) { 333 mConnectCallback = callback; 334 // We should flag this network to auto-open captive portal since this method represents 335 // the user manually connecting to a network (i.e. not auto-join). 336 mShouldAutoOpenCaptivePortal = true; 337 mWifiManager.stopRestrictingAutoJoinToSubscriptionId(); 338 if (isSaved() || isSuggestion()) { 339 if (Utils.isSimCredential(mTargetWifiConfig) 340 && !Utils.isSimPresent(mContext, mTargetWifiConfig.carrierId)) { 341 if (callback != null) { 342 mCallbackHandler.post(() -> 343 callback.onConnectResult( 344 ConnectCallback.CONNECT_STATUS_FAILURE_SIM_ABSENT)); 345 } 346 return; 347 } 348 // Saved/suggested network 349 mWifiManager.connect(mTargetWifiConfig.networkId, new ConnectActionListener()); 350 } else { 351 if (mTargetSecurityTypes.contains(SECURITY_TYPE_OWE)) { 352 // OWE network 353 final WifiConfiguration oweConfig = new WifiConfiguration(); 354 oweConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\""; 355 oweConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE); 356 mWifiManager.connect(oweConfig, new ConnectActionListener()); 357 if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN)) { 358 // Add an extra Open config for OWE transition networks 359 final WifiConfiguration openConfig = new WifiConfiguration(); 360 openConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\""; 361 openConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN); 362 mWifiManager.save(openConfig, null); 363 } 364 } else if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN)) { 365 // Open network 366 final WifiConfiguration openConfig = new WifiConfiguration(); 367 openConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\""; 368 openConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN); 369 mWifiManager.connect(openConfig, new ConnectActionListener()); 370 } else { 371 // Secure network 372 if (callback != null) { 373 mCallbackHandler.post(() -> 374 callback.onConnectResult( 375 ConnectCallback.CONNECT_STATUS_FAILURE_NO_CONFIG)); 376 } 377 } 378 } 379 } 380 381 @Override canDisconnect()382 public boolean canDisconnect() { 383 return getConnectedState() == CONNECTED_STATE_CONNECTED; 384 } 385 386 @Override disconnect(@ullable DisconnectCallback callback)387 public synchronized void disconnect(@Nullable DisconnectCallback callback) { 388 if (canDisconnect()) { 389 mCalledDisconnect = true; 390 mDisconnectCallback = callback; 391 mCallbackHandler.postDelayed(() -> { 392 if (callback != null && mCalledDisconnect) { 393 callback.onDisconnectResult( 394 DisconnectCallback.DISCONNECT_STATUS_FAILURE_UNKNOWN); 395 } 396 }, 10_000 /* delayMillis */); 397 mWifiManager.disableEphemeralNetwork("\"" + mKey.getScanResultKey().getSsid() + "\""); 398 mWifiManager.disconnect(); 399 } 400 } 401 402 @Override canForget()403 public boolean canForget() { 404 return getWifiConfiguration() != null; 405 } 406 407 @Override forget(@ullable ForgetCallback callback)408 public synchronized void forget(@Nullable ForgetCallback callback) { 409 if (canForget()) { 410 mForgetCallback = callback; 411 mWifiManager.forget(mTargetWifiConfig.networkId, new ForgetActionListener()); 412 } 413 } 414 415 @Override canSignIn()416 public synchronized boolean canSignIn() { 417 return mNetworkCapabilities != null 418 && mNetworkCapabilities.hasCapability( 419 NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL); 420 } 421 422 @Override signIn(@ullable SignInCallback callback)423 public void signIn(@Nullable SignInCallback callback) { 424 if (canSignIn()) { 425 // canSignIn() implies that this WifiEntry is the currently connected network, so use 426 // getCurrentNetwork() to start the captive portal app. 427 ((ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) 428 .startCaptivePortalApp(mWifiManager.getCurrentNetwork()); 429 } 430 } 431 432 /** 433 * Returns whether the network can be shared via QR code. 434 * See https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 435 */ 436 @Override canShare()437 public synchronized boolean canShare() { 438 if (mInjector.isDemoMode()) { 439 return false; 440 } 441 442 if (getWifiConfiguration() == null) { 443 return false; 444 } 445 446 for (int securityType : mTargetSecurityTypes) { 447 switch (securityType) { 448 case SECURITY_TYPE_OPEN: 449 case SECURITY_TYPE_OWE: 450 case SECURITY_TYPE_WEP: 451 case SECURITY_TYPE_PSK: 452 case SECURITY_TYPE_SAE: 453 return true; 454 } 455 } 456 return false; 457 } 458 459 /** 460 * Returns whether the user can use Easy Connect to onboard a device to the network. 461 * See https://www.wi-fi.org/discover-wi-fi/wi-fi-easy-connect 462 */ 463 @Override canEasyConnect()464 public synchronized boolean canEasyConnect() { 465 if (mInjector.isDemoMode()) { 466 return false; 467 } 468 469 if (getWifiConfiguration() == null) { 470 return false; 471 } 472 473 if (!mWifiManager.isEasyConnectSupported()) { 474 return false; 475 } 476 477 // DPP 1.0 only supports WPA2 and WPA3. 478 return mTargetSecurityTypes.contains(SECURITY_TYPE_PSK) 479 || mTargetSecurityTypes.contains(SECURITY_TYPE_SAE); 480 } 481 482 @Override 483 @MeteredChoice getMeteredChoice()484 public synchronized int getMeteredChoice() { 485 if (!isSuggestion() && mTargetWifiConfig != null) { 486 final int meteredOverride = mTargetWifiConfig.meteredOverride; 487 if (meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED) { 488 return METERED_CHOICE_METERED; 489 } else if (meteredOverride == WifiConfiguration.METERED_OVERRIDE_NOT_METERED) { 490 return METERED_CHOICE_UNMETERED; 491 } 492 } 493 return METERED_CHOICE_AUTO; 494 } 495 496 @Override canSetMeteredChoice()497 public boolean canSetMeteredChoice() { 498 return getWifiConfiguration() != null; 499 } 500 501 @Override setMeteredChoice(int meteredChoice)502 public synchronized void setMeteredChoice(int meteredChoice) { 503 if (!canSetMeteredChoice()) { 504 return; 505 } 506 507 if (meteredChoice == METERED_CHOICE_AUTO) { 508 mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_NONE; 509 } else if (meteredChoice == METERED_CHOICE_METERED) { 510 mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED; 511 } else if (meteredChoice == METERED_CHOICE_UNMETERED) { 512 mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_NOT_METERED; 513 } 514 mWifiManager.save(mTargetWifiConfig, null /* listener */); 515 } 516 517 @Override canSetPrivacy()518 public boolean canSetPrivacy() { 519 return isSaved(); 520 } 521 522 @Override 523 @Privacy getPrivacy()524 public synchronized int getPrivacy() { 525 if (mTargetWifiConfig != null 526 && mTargetWifiConfig.macRandomizationSetting 527 == WifiConfiguration.RANDOMIZATION_NONE) { 528 return PRIVACY_DEVICE_MAC; 529 } else { 530 return PRIVACY_RANDOMIZED_MAC; 531 } 532 } 533 534 @Override setPrivacy(int privacy)535 public synchronized void setPrivacy(int privacy) { 536 if (!canSetPrivacy()) { 537 return; 538 } 539 540 mTargetWifiConfig.macRandomizationSetting = privacy == PRIVACY_RANDOMIZED_MAC 541 ? WifiConfiguration.RANDOMIZATION_AUTO : WifiConfiguration.RANDOMIZATION_NONE; 542 mWifiManager.save(mTargetWifiConfig, null /* listener */); 543 } 544 545 @Override isAutoJoinEnabled()546 public synchronized boolean isAutoJoinEnabled() { 547 if (mTargetWifiConfig == null) { 548 return false; 549 } 550 551 return mTargetWifiConfig.allowAutojoin; 552 } 553 554 @Override canSetAutoJoinEnabled()555 public boolean canSetAutoJoinEnabled() { 556 return isSaved() || isSuggestion(); 557 } 558 559 @Override setAutoJoinEnabled(boolean enabled)560 public synchronized void setAutoJoinEnabled(boolean enabled) { 561 if (mTargetWifiConfig == null || !canSetAutoJoinEnabled()) { 562 return; 563 } 564 565 mWifiManager.allowAutojoin(mTargetWifiConfig.networkId, enabled); 566 } 567 568 @Override getSecurityString(boolean concise)569 public synchronized String getSecurityString(boolean concise) { 570 if (mTargetSecurityTypes.size() == 0) { 571 return concise ? "" : mContext.getString(R.string.wifitrackerlib_wifi_security_none); 572 } 573 if (mTargetSecurityTypes.size() == 1) { 574 final int security = mTargetSecurityTypes.get(0); 575 switch(security) { 576 case SECURITY_TYPE_EAP: 577 return concise ? mContext.getString( 578 R.string.wifitrackerlib_wifi_security_short_eap_wpa_wpa2) : 579 mContext.getString( 580 R.string.wifitrackerlib_wifi_security_eap_wpa_wpa2); 581 case SECURITY_TYPE_EAP_WPA3_ENTERPRISE: 582 return concise ? mContext.getString( 583 R.string.wifitrackerlib_wifi_security_short_eap_wpa3) : 584 mContext.getString( 585 R.string.wifitrackerlib_wifi_security_eap_wpa3); 586 case SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT: 587 return concise ? mContext.getString( 588 R.string.wifitrackerlib_wifi_security_short_eap_suiteb) : 589 mContext.getString(R.string.wifitrackerlib_wifi_security_eap_suiteb); 590 case SECURITY_TYPE_PSK: 591 return concise ? mContext.getString( 592 R.string.wifitrackerlib_wifi_security_short_wpa_wpa2) : 593 mContext.getString( 594 R.string.wifitrackerlib_wifi_security_wpa_wpa2); 595 case SECURITY_TYPE_WEP: 596 return mContext.getString(R.string.wifitrackerlib_wifi_security_wep); 597 case SECURITY_TYPE_SAE: 598 return concise ? mContext.getString( 599 R.string.wifitrackerlib_wifi_security_short_sae) : 600 mContext.getString(R.string.wifitrackerlib_wifi_security_sae); 601 case SECURITY_TYPE_OWE: 602 return concise ? mContext.getString( 603 R.string.wifitrackerlib_wifi_security_short_owe) : 604 mContext.getString(R.string.wifitrackerlib_wifi_security_owe); 605 case SECURITY_TYPE_OPEN: 606 return concise ? "" : mContext.getString( 607 R.string.wifitrackerlib_wifi_security_none); 608 } 609 } 610 if (mTargetSecurityTypes.size() == 2) { 611 if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN) 612 && mTargetSecurityTypes.contains(SECURITY_TYPE_OWE)) { 613 StringJoiner sj = new StringJoiner("/"); 614 sj.add(mContext.getString(R.string.wifitrackerlib_wifi_security_none)); 615 sj.add(concise ? mContext.getString( 616 R.string.wifitrackerlib_wifi_security_short_owe) : 617 mContext.getString(R.string.wifitrackerlib_wifi_security_owe)); 618 return sj.toString(); 619 } 620 if (mTargetSecurityTypes.contains(SECURITY_TYPE_PSK) 621 && mTargetSecurityTypes.contains(SECURITY_TYPE_SAE)) { 622 return concise ? mContext.getString( 623 R.string.wifitrackerlib_wifi_security_short_wpa_wpa2_wpa3) : 624 mContext.getString( 625 R.string.wifitrackerlib_wifi_security_wpa_wpa2_wpa3); 626 } 627 if (mTargetSecurityTypes.contains(SECURITY_TYPE_EAP) 628 && mTargetSecurityTypes.contains(SECURITY_TYPE_EAP_WPA3_ENTERPRISE)) { 629 return concise ? mContext.getString( 630 R.string.wifitrackerlib_wifi_security_short_eap_wpa_wpa2_wpa3) : 631 mContext.getString( 632 R.string.wifitrackerlib_wifi_security_eap_wpa_wpa2_wpa3); 633 } 634 } 635 // Unknown security types 636 Log.e(TAG, "Couldn't get string for security types: " + mTargetSecurityTypes); 637 return concise ? "" : mContext.getString(R.string.wifitrackerlib_wifi_security_none); 638 } 639 640 @Override shouldEditBeforeConnect()641 public synchronized boolean shouldEditBeforeConnect() { 642 WifiConfiguration wifiConfig = getWifiConfiguration(); 643 if (wifiConfig == null) { 644 return false; 645 } 646 647 // The network is disabled because of one of the authentication problems. 648 NetworkSelectionStatus networkSelectionStatus = wifiConfig.getNetworkSelectionStatus(); 649 if (networkSelectionStatus.getNetworkSelectionStatus() != NETWORK_SELECTION_ENABLED) { 650 if (networkSelectionStatus.getDisableReasonCounter(DISABLED_AUTHENTICATION_FAILURE) > 0 651 || networkSelectionStatus.getDisableReasonCounter( 652 DISABLED_BY_WRONG_PASSWORD) > 0 653 || networkSelectionStatus.getDisableReasonCounter( 654 DISABLED_AUTHENTICATION_NO_CREDENTIALS) > 0) { 655 return true; 656 } 657 } 658 659 return false; 660 } 661 662 @WorkerThread updateScanResultInfo(@ullable List<ScanResult> scanResults)663 synchronized void updateScanResultInfo(@Nullable List<ScanResult> scanResults) 664 throws IllegalArgumentException { 665 if (scanResults == null) scanResults = new ArrayList<>(); 666 667 final String ssid = mKey.getScanResultKey().getSsid(); 668 for (ScanResult scan : scanResults) { 669 if (!TextUtils.equals(scan.SSID, ssid)) { 670 throw new IllegalArgumentException( 671 "Attempted to update with wrong SSID! Expected: " 672 + ssid + ", Actual: " + scan.SSID + ", ScanResult: " + scan); 673 } 674 } 675 // Populate the cached scan result map 676 mMatchingScanResults.clear(); 677 final Set<Integer> keySecurityTypes = mKey.getScanResultKey().getSecurityTypes(); 678 for (ScanResult scan : scanResults) { 679 for (int security : getSecurityTypesFromScanResult(scan)) { 680 if (!keySecurityTypes.contains(security) || !isSecurityTypeSupported(security)) { 681 continue; 682 } 683 if (!mMatchingScanResults.containsKey(security)) { 684 mMatchingScanResults.put(security, new ArrayList<>()); 685 } 686 mMatchingScanResults.get(security).add(scan); 687 } 688 } 689 690 updateSecurityTypes(); 691 updateTargetScanResultInfo(); 692 notifyOnUpdated(); 693 } 694 updateTargetScanResultInfo()695 private synchronized void updateTargetScanResultInfo() { 696 // Update the level using the scans matching the target security type 697 final ScanResult bestScanResult = getBestScanResultByLevel(mTargetScanResults); 698 699 if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) { 700 mLevel = bestScanResult != null 701 ? mWifiManager.calculateSignalLevel(bestScanResult.level) 702 : WIFI_LEVEL_UNREACHABLE; 703 // Average speed is used to prevent speed label flickering from multiple APs. 704 mSpeed = getAverageSpeedFromScanResults(mScoreCache, mTargetScanResults); 705 } 706 } 707 708 @WorkerThread 709 @Override updateNetworkCapabilities(@ullable NetworkCapabilities capabilities)710 synchronized void updateNetworkCapabilities(@Nullable NetworkCapabilities capabilities) { 711 super.updateNetworkCapabilities(capabilities); 712 713 // Auto-open an available captive portal if the user manually connected to this network. 714 if (canSignIn() && mShouldAutoOpenCaptivePortal) { 715 mShouldAutoOpenCaptivePortal = false; 716 signIn(null /* callback */); 717 } 718 } 719 720 @WorkerThread onScoreCacheUpdated()721 synchronized void onScoreCacheUpdated() { 722 if (mWifiInfo != null) { 723 mSpeed = getSpeedFromWifiInfo(mScoreCache, mWifiInfo); 724 } else { 725 // Average speed is used to prevent speed label flickering from multiple APs. 726 mSpeed = getAverageSpeedFromScanResults(mScoreCache, mTargetScanResults); 727 } 728 notifyOnUpdated(); 729 } 730 731 @WorkerThread updateConfig(@ullable List<WifiConfiguration> wifiConfigs)732 synchronized void updateConfig(@Nullable List<WifiConfiguration> wifiConfigs) 733 throws IllegalArgumentException { 734 if (wifiConfigs == null) { 735 wifiConfigs = Collections.emptyList(); 736 } 737 738 final ScanResultKey scanResultKey = mKey.getScanResultKey(); 739 final String ssid = scanResultKey.getSsid(); 740 final Set<Integer> securityTypes = scanResultKey.getSecurityTypes(); 741 mMatchingWifiConfigs.clear(); 742 for (WifiConfiguration config : wifiConfigs) { 743 if (!TextUtils.equals(ssid, sanitizeSsid(config.SSID))) { 744 throw new IllegalArgumentException( 745 "Attempted to update with wrong SSID!" 746 + " Expected: " + ssid 747 + ", Actual: " + sanitizeSsid(config.SSID) 748 + ", Config: " + config); 749 } 750 for (int securityType : getSecurityTypesFromWifiConfiguration(config)) { 751 if (!securityTypes.contains(securityType)) { 752 throw new IllegalArgumentException( 753 "Attempted to update with wrong security!" 754 + " Expected one of: " + securityTypes 755 + ", Actual: " + securityType 756 + ", Config: " + config); 757 } 758 if (isSecurityTypeSupported(securityType)) { 759 mMatchingWifiConfigs.put(securityType, config); 760 } 761 } 762 } 763 updateSecurityTypes(); 764 updateTargetScanResultInfo(); 765 notifyOnUpdated(); 766 } 767 isSecurityTypeSupported(int security)768 private boolean isSecurityTypeSupported(int security) { 769 switch (security) { 770 case SECURITY_TYPE_SAE: 771 return mIsWpa3SaeSupported; 772 case SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT: 773 return mIsWpa3SuiteBSupported; 774 case SECURITY_TYPE_OWE: 775 return mIsEnhancedOpenSupported; 776 default: 777 return true; 778 } 779 } 780 781 @Override updateSecurityTypes()782 protected synchronized void updateSecurityTypes() { 783 mTargetSecurityTypes.clear(); 784 if (mWifiInfo != null) { 785 final int wifiInfoSecurity = mWifiInfo.getCurrentSecurityType(); 786 if (wifiInfoSecurity != SECURITY_TYPE_UNKNOWN) { 787 mTargetSecurityTypes.add(mWifiInfo.getCurrentSecurityType()); 788 } 789 } 790 791 Set<Integer> configSecurityTypes = mMatchingWifiConfigs.keySet(); 792 if (mTargetSecurityTypes.isEmpty() && mKey.isTargetingNewNetworks()) { 793 // If we are targeting new networks for configuration, then we should select the 794 // security type of all visible scan results if we don't have any configs that 795 // can connect to them. This will let us configure this entry as a new network. 796 boolean configMatchesScans = false; 797 Set<Integer> scanSecurityTypes = mMatchingScanResults.keySet(); 798 for (int configSecurity : configSecurityTypes) { 799 if (scanSecurityTypes.contains(configSecurity)) { 800 configMatchesScans = true; 801 break; 802 } 803 } 804 if (!configMatchesScans) { 805 mTargetSecurityTypes.addAll(scanSecurityTypes); 806 } 807 } 808 809 // Use security types of any configs we have 810 if (mTargetSecurityTypes.isEmpty()) { 811 mTargetSecurityTypes.addAll(configSecurityTypes); 812 } 813 814 // Default to the key security types. This shouldn't happen since we should always have 815 // scans or configs. 816 if (mTargetSecurityTypes.isEmpty()) { 817 mTargetSecurityTypes.addAll(mKey.getScanResultKey().getSecurityTypes()); 818 } 819 820 // The target wifi config should match the security type we return in getSecurity(), since 821 // clients (QR code/DPP, modify network page) may expect them to match. 822 mTargetWifiConfig = mMatchingWifiConfigs.get( 823 getSingleSecurityTypeFromMultipleSecurityTypes(mTargetSecurityTypes)); 824 // Collect target scan results in a set to remove duplicates when one scan matches multiple 825 // security types. 826 Set<ScanResult> targetScanResultSet = new ArraySet<>(); 827 for (int security : mTargetSecurityTypes) { 828 if (mMatchingScanResults.containsKey(security)) { 829 targetScanResultSet.addAll(mMatchingScanResults.get(security)); 830 } 831 } 832 mTargetScanResults.clear(); 833 mTargetScanResults.addAll(targetScanResultSet); 834 } 835 836 /** 837 * Sets whether the suggested config for this entry is shareable to the user or not. 838 */ 839 @WorkerThread setUserShareable(boolean isUserShareable)840 synchronized void setUserShareable(boolean isUserShareable) { 841 mIsUserShareable = isUserShareable; 842 } 843 844 /** 845 * Returns whether the suggested config for this entry is shareable to the user or not. 846 */ 847 @WorkerThread isUserShareable()848 synchronized boolean isUserShareable() { 849 return mIsUserShareable; 850 } 851 852 @WorkerThread connectionInfoMatches(@onNull WifiInfo wifiInfo, @NonNull NetworkInfo networkInfo)853 protected synchronized boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo, 854 @NonNull NetworkInfo networkInfo) { 855 if (wifiInfo.isPasspointAp() || wifiInfo.isOsuAp()) { 856 return false; 857 } 858 for (WifiConfiguration config : mMatchingWifiConfigs.values()) { 859 if (config.networkId == wifiInfo.getNetworkId()) { 860 return true; 861 } 862 } 863 return false; 864 } 865 updateRecommendationServiceLabel()866 private synchronized void updateRecommendationServiceLabel() { 867 final NetworkScorerAppData scorer = ((NetworkScoreManager) mContext 868 .getSystemService(Context.NETWORK_SCORE_SERVICE)).getActiveScorer(); 869 if (scorer != null) { 870 mRecommendationServiceLabel = scorer.getRecommendationServiceLabel(); 871 } 872 } 873 874 @NonNull ssidAndSecurityTypeToStandardWifiEntryKey( @onNull String ssid, int security)875 static StandardWifiEntryKey ssidAndSecurityTypeToStandardWifiEntryKey( 876 @NonNull String ssid, int security) { 877 return ssidAndSecurityTypeToStandardWifiEntryKey( 878 ssid, security, false /* isTargetingNewNetworks */); 879 } 880 881 @NonNull ssidAndSecurityTypeToStandardWifiEntryKey( @onNull String ssid, int security, boolean isTargetingNewNetworks)882 static StandardWifiEntryKey ssidAndSecurityTypeToStandardWifiEntryKey( 883 @NonNull String ssid, int security, boolean isTargetingNewNetworks) { 884 return new StandardWifiEntryKey( 885 new ScanResultKey(ssid, Collections.singletonList(security)), 886 isTargetingNewNetworks); 887 } 888 889 @Override getScanResultDescription()890 protected synchronized String getScanResultDescription() { 891 if (mTargetScanResults.size() == 0) { 892 return ""; 893 } 894 895 final StringBuilder description = new StringBuilder(); 896 description.append("["); 897 description.append(getScanResultDescription(MIN_FREQ_24GHZ, MAX_FREQ_24GHZ)).append(";"); 898 description.append(getScanResultDescription(MIN_FREQ_5GHZ, MAX_FREQ_5GHZ)).append(";"); 899 description.append(getScanResultDescription(MIN_FREQ_6GHZ, MAX_FREQ_6GHZ)).append(";"); 900 description.append(getScanResultDescription(MIN_FREQ_60GHZ, MAX_FREQ_60GHZ)); 901 description.append("]"); 902 return description.toString(); 903 } 904 getScanResultDescription(int minFrequency, int maxFrequency)905 private synchronized String getScanResultDescription(int minFrequency, int maxFrequency) { 906 final List<ScanResult> scanResults = mTargetScanResults.stream() 907 .filter(scanResult -> scanResult.frequency >= minFrequency 908 && scanResult.frequency <= maxFrequency) 909 .sorted(Comparator.comparingInt(scanResult -> -1 * scanResult.level)) 910 .collect(Collectors.toList()); 911 912 final int scanResultCount = scanResults.size(); 913 if (scanResultCount == 0) { 914 return ""; 915 } 916 917 final StringBuilder description = new StringBuilder(); 918 description.append("(").append(scanResultCount).append(")"); 919 if (scanResultCount > MAX_VERBOSE_LOG_DISPLAY_SCANRESULT_COUNT) { 920 final int maxLavel = scanResults.stream() 921 .mapToInt(scanResult -> scanResult.level).max().getAsInt(); 922 description.append("max=").append(maxLavel).append(","); 923 } 924 final long nowMs = SystemClock.elapsedRealtime(); 925 scanResults.forEach(scanResult -> 926 description.append(getScanResultDescription(scanResult, nowMs))); 927 return description.toString(); 928 } 929 getScanResultDescription(ScanResult scanResult, long nowMs)930 private synchronized String getScanResultDescription(ScanResult scanResult, long nowMs) { 931 final StringBuilder description = new StringBuilder(); 932 description.append(" \n{"); 933 description.append(scanResult.BSSID); 934 if (mWifiInfo != null && scanResult.BSSID.equals(mWifiInfo.getBSSID())) { 935 description.append("*"); 936 } 937 description.append("=").append(scanResult.frequency); 938 description.append(",").append(scanResult.level); 939 final int ageSeconds = (int) (nowMs - scanResult.timestamp / 1000) / 1000; 940 description.append(",").append(ageSeconds).append("s"); 941 description.append("}"); 942 return description.toString(); 943 } 944 945 @Override getNetworkSelectionDescription()946 String getNetworkSelectionDescription() { 947 return Utils.getNetworkSelectionDescription(getWifiConfiguration()); 948 } 949 950 /** 951 * Class that identifies a unique StandardWifiEntry by the following identifiers 952 * 1) ScanResult key (SSID + grouped security types) 953 * 2) Suggestion profile key 954 * 3) Is network request or not 955 * 4) Should prioritize configuring a new network (i.e. target the security type of an 956 * in-range unsaved network, rather than a config that has no scans) 957 */ 958 static class StandardWifiEntryKey { 959 private static final String KEY_SCAN_RESULT_KEY = "SCAN_RESULT_KEY"; 960 private static final String KEY_SUGGESTION_PROFILE_KEY = "SUGGESTION_PROFILE_KEY"; 961 private static final String KEY_IS_NETWORK_REQUEST = "IS_NETWORK_REQUEST"; 962 private static final String KEY_IS_TARGETING_NEW_NETWORKS = "IS_TARGETING_NEW_NETWORKS"; 963 964 @NonNull private ScanResultKey mScanResultKey; 965 @Nullable private String mSuggestionProfileKey; 966 private boolean mIsNetworkRequest; 967 private boolean mIsTargetingNewNetworks = false; 968 969 /** 970 * Creates a StandardWifiEntryKey matching a ScanResultKey 971 */ StandardWifiEntryKey(@onNull ScanResultKey scanResultKey)972 StandardWifiEntryKey(@NonNull ScanResultKey scanResultKey) { 973 this(scanResultKey, false /* isTargetingNewNetworks */); 974 } 975 976 /** 977 * Creates a StandardWifiEntryKey matching a ScanResultKey and sets whether the entry 978 * should target new networks or not. 979 */ StandardWifiEntryKey(@onNull ScanResultKey scanResultKey, boolean isTargetingNewNetworks)980 StandardWifiEntryKey(@NonNull ScanResultKey scanResultKey, boolean isTargetingNewNetworks) { 981 mScanResultKey = scanResultKey; 982 mIsTargetingNewNetworks = isTargetingNewNetworks; 983 } 984 985 /** 986 * Creates a StandardWifiEntryKey matching a WifiConfiguration 987 */ StandardWifiEntryKey(@onNull WifiConfiguration config)988 StandardWifiEntryKey(@NonNull WifiConfiguration config) { 989 this(config, false /* isTargetingNewNetworks */); 990 } 991 992 /** 993 * Creates a StandardWifiEntryKey matching a WifiConfiguration and sets whether the entry 994 * should target new networks or not. 995 */ StandardWifiEntryKey(@onNull WifiConfiguration config, boolean isTargetingNewNetworks)996 StandardWifiEntryKey(@NonNull WifiConfiguration config, boolean isTargetingNewNetworks) { 997 mScanResultKey = new ScanResultKey(config); 998 if (config.fromWifiNetworkSuggestion) { 999 mSuggestionProfileKey = new StringJoiner(",") 1000 .add(config.creatorName) 1001 .add(String.valueOf(config.carrierId)) 1002 .add(String.valueOf(config.subscriptionId)) 1003 .toString(); 1004 } else if (config.fromWifiNetworkSpecifier) { 1005 mIsNetworkRequest = true; 1006 } 1007 mIsTargetingNewNetworks = isTargetingNewNetworks; 1008 } 1009 1010 /** 1011 * Creates a StandardWifiEntryKey from its String representation. 1012 */ StandardWifiEntryKey(@onNull String string)1013 StandardWifiEntryKey(@NonNull String string) { 1014 mScanResultKey = new ScanResultKey(); 1015 if (!string.startsWith(KEY_PREFIX)) { 1016 Log.e(TAG, "String key does not start with key prefix!"); 1017 return; 1018 } 1019 try { 1020 final JSONObject keyJson = new JSONObject(string.substring(KEY_PREFIX.length())); 1021 if (keyJson.has(KEY_SCAN_RESULT_KEY)) { 1022 mScanResultKey = new ScanResultKey(keyJson.getString(KEY_SCAN_RESULT_KEY)); 1023 } 1024 if (keyJson.has(KEY_SUGGESTION_PROFILE_KEY)) { 1025 mSuggestionProfileKey = keyJson.getString(KEY_SUGGESTION_PROFILE_KEY); 1026 } 1027 if (keyJson.has(KEY_IS_NETWORK_REQUEST)) { 1028 mIsNetworkRequest = keyJson.getBoolean(KEY_IS_NETWORK_REQUEST); 1029 } 1030 if (keyJson.has(KEY_IS_TARGETING_NEW_NETWORKS)) { 1031 mIsTargetingNewNetworks = keyJson.getBoolean( 1032 KEY_IS_TARGETING_NEW_NETWORKS); 1033 } 1034 } catch (JSONException e) { 1035 Log.e(TAG, "JSONException while converting StandardWifiEntryKey to string: " + e); 1036 } 1037 } 1038 1039 /** 1040 * Returns the JSON String representation of this StandardWifiEntryKey. 1041 */ 1042 @Override toString()1043 public String toString() { 1044 final JSONObject keyJson = new JSONObject(); 1045 try { 1046 if (mScanResultKey != null) { 1047 keyJson.put(KEY_SCAN_RESULT_KEY, mScanResultKey.toString()); 1048 } 1049 if (mSuggestionProfileKey != null) { 1050 keyJson.put(KEY_SUGGESTION_PROFILE_KEY, mSuggestionProfileKey); 1051 } 1052 if (mIsNetworkRequest) { 1053 keyJson.put(KEY_IS_NETWORK_REQUEST, mIsNetworkRequest); 1054 } 1055 if (mIsTargetingNewNetworks) { 1056 keyJson.put(KEY_IS_TARGETING_NEW_NETWORKS, mIsTargetingNewNetworks); 1057 } 1058 } catch (JSONException e) { 1059 Log.wtf(TAG, "JSONException while converting StandardWifiEntryKey to string: " + e); 1060 } 1061 return KEY_PREFIX + keyJson.toString(); 1062 } 1063 1064 /** 1065 * Returns the ScanResultKey of this StandardWifiEntryKey to match against ScanResults 1066 */ getScanResultKey()1067 @NonNull ScanResultKey getScanResultKey() { 1068 return mScanResultKey; 1069 } 1070 getSuggestionProfileKey()1071 @Nullable String getSuggestionProfileKey() { 1072 return mSuggestionProfileKey; 1073 } 1074 isNetworkRequest()1075 boolean isNetworkRequest() { 1076 return mIsNetworkRequest; 1077 } 1078 isTargetingNewNetworks()1079 boolean isTargetingNewNetworks() { 1080 return mIsTargetingNewNetworks; 1081 } 1082 1083 @Override equals(Object o)1084 public boolean equals(Object o) { 1085 if (this == o) return true; 1086 if (o == null || getClass() != o.getClass()) return false; 1087 StandardWifiEntryKey that = (StandardWifiEntryKey) o; 1088 return Objects.equals(mScanResultKey, that.mScanResultKey) 1089 && TextUtils.equals(mSuggestionProfileKey, that.mSuggestionProfileKey) 1090 && mIsNetworkRequest == that.mIsNetworkRequest; 1091 } 1092 1093 @Override hashCode()1094 public int hashCode() { 1095 return Objects.hash(mScanResultKey, mSuggestionProfileKey, mIsNetworkRequest); 1096 } 1097 } 1098 1099 /** 1100 * Class for matching ScanResults to StandardWifiEntry by SSID and security type grouping. 1101 */ 1102 static class ScanResultKey { 1103 private static final String KEY_SSID = "SSID"; 1104 private static final String KEY_SECURITY_TYPES = "SECURITY_TYPES"; 1105 1106 @Nullable private String mSsid; 1107 @NonNull private Set<Integer> mSecurityTypes = new ArraySet<>(); 1108 ScanResultKey()1109 ScanResultKey() { 1110 } 1111 ScanResultKey(@ullable String ssid, List<Integer> securityTypes)1112 ScanResultKey(@Nullable String ssid, List<Integer> securityTypes) { 1113 mSsid = ssid; 1114 for (int security : securityTypes) { 1115 mSecurityTypes.add(security); 1116 // Add any security types that merge to the same WifiEntry 1117 switch (security) { 1118 // Group OPEN and OWE networks together 1119 case SECURITY_TYPE_OPEN: 1120 mSecurityTypes.add(SECURITY_TYPE_OWE); 1121 break; 1122 case SECURITY_TYPE_OWE: 1123 mSecurityTypes.add(SECURITY_TYPE_OPEN); 1124 break; 1125 // Group PSK and SAE networks together 1126 case SECURITY_TYPE_PSK: 1127 mSecurityTypes.add(SECURITY_TYPE_SAE); 1128 break; 1129 case SECURITY_TYPE_SAE: 1130 mSecurityTypes.add(SECURITY_TYPE_PSK); 1131 break; 1132 // Group EAP and EAP_WPA3_ENTERPRISE networks together 1133 case SECURITY_TYPE_EAP: 1134 mSecurityTypes.add(SECURITY_TYPE_EAP_WPA3_ENTERPRISE); 1135 break; 1136 case SECURITY_TYPE_EAP_WPA3_ENTERPRISE: 1137 mSecurityTypes.add(SECURITY_TYPE_EAP); 1138 break; 1139 } 1140 } 1141 } 1142 1143 /** 1144 * Creates a ScanResultKey from a ScanResult's SSID and security type grouping. 1145 * @param scanResult 1146 */ ScanResultKey(@onNull ScanResult scanResult)1147 ScanResultKey(@NonNull ScanResult scanResult) { 1148 this(scanResult.SSID, getSecurityTypesFromScanResult(scanResult)); 1149 } 1150 1151 /** 1152 * Creates a ScanResultKey from a WifiConfiguration's SSID and security type grouping. 1153 */ ScanResultKey(@onNull WifiConfiguration wifiConfiguration)1154 ScanResultKey(@NonNull WifiConfiguration wifiConfiguration) { 1155 this(sanitizeSsid(wifiConfiguration.SSID), 1156 getSecurityTypesFromWifiConfiguration(wifiConfiguration)); 1157 } 1158 1159 /** 1160 * Creates a ScanResultKey from its String representation. 1161 */ ScanResultKey(@onNull String string)1162 ScanResultKey(@NonNull String string) { 1163 try { 1164 final JSONObject keyJson = new JSONObject(string); 1165 mSsid = keyJson.getString(KEY_SSID); 1166 final JSONArray securityTypesJson = 1167 keyJson.getJSONArray(KEY_SECURITY_TYPES); 1168 for (int i = 0; i < securityTypesJson.length(); i++) { 1169 mSecurityTypes.add(securityTypesJson.getInt(i)); 1170 } 1171 } catch (JSONException e) { 1172 Log.wtf(TAG, "JSONException while constructing ScanResultKey from string: " + e); 1173 } 1174 } 1175 1176 /** 1177 * Returns the JSON String representation of this ScanResultEntry. 1178 */ 1179 @Override toString()1180 public String toString() { 1181 final JSONObject keyJson = new JSONObject(); 1182 try { 1183 if (mSsid != null) { 1184 keyJson.put(KEY_SSID, mSsid); 1185 } 1186 if (!mSecurityTypes.isEmpty()) { 1187 final JSONArray securityTypesJson = new JSONArray(); 1188 for (int security : mSecurityTypes) { 1189 securityTypesJson.put(security); 1190 } 1191 keyJson.put(KEY_SECURITY_TYPES, securityTypesJson); 1192 } 1193 } catch (JSONException e) { 1194 Log.e(TAG, "JSONException while converting ScanResultKey to string: " + e); 1195 } 1196 return keyJson.toString(); 1197 } 1198 getSsid()1199 @Nullable String getSsid() { 1200 return mSsid; 1201 } 1202 getSecurityTypes()1203 @NonNull Set<Integer> getSecurityTypes() { 1204 return mSecurityTypes; 1205 } 1206 1207 @Override equals(Object o)1208 public boolean equals(Object o) { 1209 if (this == o) return true; 1210 if (o == null || getClass() != o.getClass()) return false; 1211 ScanResultKey that = (ScanResultKey) o; 1212 return TextUtils.equals(mSsid, that.mSsid) 1213 && mSecurityTypes.equals(that.mSecurityTypes); 1214 } 1215 1216 @Override hashCode()1217 public int hashCode() { 1218 return Objects.hash(mSsid, mSecurityTypes); 1219 } 1220 } 1221 } 1222