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