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