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