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