1 /*
2  * Copyright (C) 2020 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.WifiInfo.DEFAULT_MAC_ADDRESS;
20 import static android.net.wifi.WifiInfo.SECURITY_TYPE_PASSPOINT_R1_R2;
21 import static android.net.wifi.WifiInfo.SECURITY_TYPE_PASSPOINT_R3;
22 import static android.net.wifi.WifiInfo.SECURITY_TYPE_UNKNOWN;
23 import static android.net.wifi.WifiInfo.sanitizeSsid;
24 
25 import static androidx.core.util.Preconditions.checkNotNull;
26 
27 import static com.android.wifitrackerlib.Utils.getAutoConnectDescription;
28 import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
29 import static com.android.wifitrackerlib.Utils.getConnectedDescription;
30 import static com.android.wifitrackerlib.Utils.getConnectingDescription;
31 import static com.android.wifitrackerlib.Utils.getDisconnectedDescription;
32 import static com.android.wifitrackerlib.Utils.getMeteredDescription;
33 import static com.android.wifitrackerlib.Utils.getVerboseSummary;
34 
35 import android.annotation.SuppressLint;
36 import android.content.Context;
37 import android.net.ConnectivityManager;
38 import android.net.Network;
39 import android.net.NetworkCapabilities;
40 import android.net.wifi.ScanResult;
41 import android.net.wifi.WifiConfiguration;
42 import android.net.wifi.WifiInfo;
43 import android.net.wifi.WifiManager;
44 import android.net.wifi.hotspot2.PasspointConfiguration;
45 import android.os.Handler;
46 import android.text.TextUtils;
47 import android.util.ArraySet;
48 import android.util.Log;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.annotation.WorkerThread;
53 import androidx.core.os.BuildCompat;
54 
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collections;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.StringJoiner;
61 
62 /**
63  * WifiEntry representation of a subscribed Passpoint network, uniquely identified by FQDN.
64  */
65 public class PasspointWifiEntry extends WifiEntry implements WifiEntry.WifiEntryCallback {
66     static final String TAG = "PasspointWifiEntry";
67     public static final String KEY_PREFIX = "PasspointWifiEntry:";
68 
69     private final List<ScanResult> mCurrentHomeScanResults = new ArrayList<>();
70     private final List<ScanResult> mCurrentRoamingScanResults = new ArrayList<>();
71 
72     @NonNull private final String mKey;
73     @NonNull private final String mUniqueId;
74     @NonNull private final String mFqdn;
75     @NonNull private final String mFriendlyName;
76     @Nullable
77     private PasspointConfiguration mPasspointConfig;
78     @Nullable private WifiConfiguration mWifiConfig;
79     private List<Integer> mTargetSecurityTypes =
80             Arrays.asList(SECURITY_TYPE_PASSPOINT_R1_R2, SECURITY_TYPE_PASSPOINT_R3);
81 
82     private OsuWifiEntry mOsuWifiEntry;
83     private boolean mShouldAutoOpenCaptivePortal = false;
84 
85     protected long mSubscriptionExpirationTimeInMillis;
86 
87     // PasspointConfiguration#setMeteredOverride(int meteredOverride) is a hide API and we can't
88     // set it in PasspointWifiEntry#setMeteredChoice(int meteredChoice).
89     // For PasspointWifiEntry#getMeteredChoice() to return correct value right after
90     // PasspointWifiEntry#setMeteredChoice(int meteredChoice), cache
91     // PasspointConfiguration#getMeteredOverride() in this variable.
92     private int mMeteredOverride = METERED_CHOICE_AUTO;
93 
94     /**
95      * Create a PasspointWifiEntry with the associated PasspointConfiguration
96      */
PasspointWifiEntry( @onNull WifiTrackerInjector injector, @NonNull Handler callbackHandler, @NonNull PasspointConfiguration passpointConfig, @NonNull WifiManager wifiManager, boolean forSavedNetworksPage)97     PasspointWifiEntry(
98             @NonNull WifiTrackerInjector injector,
99             @NonNull Handler callbackHandler,
100             @NonNull PasspointConfiguration passpointConfig,
101             @NonNull WifiManager wifiManager,
102             boolean forSavedNetworksPage) throws IllegalArgumentException {
103         super(injector, callbackHandler, wifiManager, forSavedNetworksPage);
104 
105         checkNotNull(passpointConfig, "Cannot construct with null PasspointConfiguration!");
106         mPasspointConfig = passpointConfig;
107         mUniqueId = passpointConfig.getUniqueId();
108         mKey = uniqueIdToPasspointWifiEntryKey(mUniqueId);
109         mFqdn = passpointConfig.getHomeSp().getFqdn();
110         checkNotNull(mFqdn, "Cannot construct with null PasspointConfiguration FQDN!");
111         mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
112         mSubscriptionExpirationTimeInMillis =
113                 passpointConfig.getSubscriptionExpirationTimeMillis();
114         mMeteredOverride = mPasspointConfig.getMeteredOverride();
115     }
116 
117     /**
118      * Create a PasspointWifiEntry with the associated WifiConfiguration for use with network
119      * suggestions, since WifiManager#getAllMatchingWifiConfigs() does not provide a corresponding
120      * PasspointConfiguration.
121      */
PasspointWifiEntry( @onNull WifiTrackerInjector injector, @NonNull Context context, @NonNull Handler callbackHandler, @NonNull WifiConfiguration wifiConfig, @NonNull WifiManager wifiManager, boolean forSavedNetworksPage)122     PasspointWifiEntry(
123             @NonNull WifiTrackerInjector injector,
124             @NonNull Context context, @NonNull Handler callbackHandler,
125             @NonNull WifiConfiguration wifiConfig,
126             @NonNull WifiManager wifiManager,
127             boolean forSavedNetworksPage) throws IllegalArgumentException {
128         super(injector, callbackHandler, wifiManager, forSavedNetworksPage);
129 
130         checkNotNull(wifiConfig, "Cannot construct with null WifiConfiguration!");
131         if (!wifiConfig.isPasspoint()) {
132             throw new IllegalArgumentException("Given WifiConfiguration is not for Passpoint!");
133         }
134         mWifiConfig = wifiConfig;
135         mUniqueId = wifiConfig.getKey();
136         mKey = uniqueIdToPasspointWifiEntryKey(mUniqueId);
137         mFqdn = wifiConfig.FQDN;
138         checkNotNull(mFqdn, "Cannot construct with null WifiConfiguration FQDN!");
139         mFriendlyName = mWifiConfig.providerFriendlyName;
140     }
141 
142     @Override
getKey()143     public String getKey() {
144         return mKey;
145     }
146 
147     @Override
148     @ConnectedState
getConnectedState()149     public synchronized int getConnectedState() {
150         if (isExpired()) {
151             if (super.getConnectedState() == CONNECTED_STATE_DISCONNECTED
152                     && mOsuWifiEntry != null) {
153                 return mOsuWifiEntry.getConnectedState();
154             }
155         }
156         return super.getConnectedState();
157     }
158 
159     @Override
getTitle()160     public String getTitle() {
161         return mFriendlyName;
162     }
163 
164     @Override
getSummary(boolean concise)165     public synchronized String getSummary(boolean concise) {
166         StringJoiner sj = new StringJoiner(mContext.getString(
167                 R.string.wifitrackerlib_summary_separator));
168 
169         if (isExpired()) {
170             if (mOsuWifiEntry != null) {
171                 sj.add(mOsuWifiEntry.getSummary(concise));
172             } else {
173                 sj.add(mContext.getString(R.string.wifitrackerlib_wifi_passpoint_expired));
174             }
175         } else {
176             final String connectedStateDescription;
177             final @ConnectedState int connectedState = getConnectedState();
178             switch (connectedState) {
179                 case CONNECTED_STATE_DISCONNECTED:
180                     connectedStateDescription = getDisconnectedDescription(mInjector, mContext,
181                             mWifiConfig,
182                             mForSavedNetworksPage,
183                             concise);
184                     break;
185                 case CONNECTED_STATE_CONNECTING:
186                     connectedStateDescription = getConnectingDescription(mContext, mNetworkInfo);
187                     break;
188                 case CONNECTED_STATE_CONNECTED:
189                     if (mNetworkCapabilities == null) {
190                         Log.e(TAG, "Tried to get CONNECTED description, but mNetworkCapabilities"
191                                 + " was unexpectedly null!");
192                         connectedStateDescription = null;
193                         break;
194                     }
195                     connectedStateDescription = getConnectedDescription(mContext,
196                             mWifiConfig,
197                             mNetworkCapabilities,
198                             isDefaultNetwork(),
199                             isLowQuality(),
200                             mConnectivityReport);
201                     break;
202                 default:
203                     Log.e(TAG, "getConnectedState() returned unknown state: " + connectedState);
204                     connectedStateDescription = null;
205             }
206             if (!TextUtils.isEmpty(connectedStateDescription)) {
207                 sj.add(connectedStateDescription);
208             }
209         }
210 
211         String autoConnectDescription = getAutoConnectDescription(mContext, this);
212         if (!TextUtils.isEmpty(autoConnectDescription)) {
213             sj.add(autoConnectDescription);
214         }
215 
216         String meteredDescription = getMeteredDescription(mContext, this);
217         if (!TextUtils.isEmpty(meteredDescription)) {
218             sj.add(meteredDescription);
219         }
220 
221         if (!concise && isVerboseSummaryEnabled()) {
222             String verboseSummary = getVerboseSummary(this);
223             if (!TextUtils.isEmpty(verboseSummary)) {
224                 sj.add(verboseSummary);
225             }
226         }
227 
228         return sj.toString();
229     }
230 
231     @Override
shouldShowSsid()232     public boolean shouldShowSsid() {
233         return true;
234     }
235 
236     @Override
getSsid()237     public synchronized String getSsid() {
238         if (mWifiInfo != null) {
239             return sanitizeSsid(mWifiInfo.getSSID());
240         }
241 
242         return mWifiConfig != null ? sanitizeSsid(mWifiConfig.SSID) : null;
243     }
244 
getAllUtf8Ssids()245     synchronized Set<String> getAllUtf8Ssids() {
246         Set<String> allSsids = new ArraySet<>();
247         for (ScanResult scan : mCurrentHomeScanResults) {
248             allSsids.add(scan.SSID);
249         }
250         for (ScanResult scan : mCurrentRoamingScanResults) {
251             allSsids.add(scan.SSID);
252         }
253         return allSsids;
254     }
255 
256     @Override
getSecurityTypes()257     public synchronized List<Integer> getSecurityTypes() {
258         return new ArrayList<>(mTargetSecurityTypes);
259     }
260 
261     @Override
262     @SuppressLint("HardwareIds")
getMacAddress()263     public synchronized String getMacAddress() {
264         if (mWifiInfo != null) {
265             final String wifiInfoMac = mWifiInfo.getMacAddress();
266             if (!TextUtils.isEmpty(wifiInfoMac)
267                     && !TextUtils.equals(wifiInfoMac, DEFAULT_MAC_ADDRESS)) {
268                 return wifiInfoMac;
269             }
270         }
271         if (mWifiConfig == null || getPrivacy() != PRIVACY_RANDOMIZED_MAC) {
272             final String[] factoryMacs = mWifiManager.getFactoryMacAddresses();
273             if (factoryMacs.length > 0) {
274                 return factoryMacs[0];
275             }
276             return null;
277         }
278         return mWifiConfig.getRandomizedMacAddress().toString();
279     }
280 
281     @Override
isMetered()282     public synchronized boolean isMetered() {
283         return getMeteredChoice() == METERED_CHOICE_METERED
284                 || (mWifiConfig != null && mWifiConfig.meteredHint);
285     }
286 
287     @Override
isSuggestion()288     public synchronized boolean isSuggestion() {
289         return mWifiConfig != null && mWifiConfig.fromWifiNetworkSuggestion;
290     }
291 
292     @Override
isSubscription()293     public synchronized boolean isSubscription() {
294         return mPasspointConfig != null;
295     }
296 
297     @Override
canConnect()298     public synchronized boolean canConnect() {
299         if (isExpired()) {
300             return mOsuWifiEntry != null && mOsuWifiEntry.canConnect();
301         }
302 
303         return mLevel != WIFI_LEVEL_UNREACHABLE
304                 && getConnectedState() == CONNECTED_STATE_DISCONNECTED && mWifiConfig != null;
305     }
306 
307     @Override
connect(@ullable ConnectCallback callback)308     public synchronized void connect(@Nullable ConnectCallback callback) {
309         if (isExpired()) {
310             if (mOsuWifiEntry != null) {
311                 mOsuWifiEntry.connect(callback);
312                 return;
313             }
314         }
315         // We should flag this network to auto-open captive portal since this method represents
316         // the user manually connecting to a network (i.e. not auto-join).
317         mShouldAutoOpenCaptivePortal = true;
318         mConnectCallback = callback;
319 
320         if (mWifiConfig == null) {
321             // We should not be able to call connect() if mWifiConfig is null
322             new ConnectActionListener().onFailure(0);
323         }
324         mWifiManager.stopRestrictingAutoJoinToSubscriptionId();
325         mWifiManager.connect(mWifiConfig, new ConnectActionListener());
326     }
327 
328     @Override
canDisconnect()329     public boolean canDisconnect() {
330         return getConnectedState() == CONNECTED_STATE_CONNECTED;
331     }
332 
333     @Override
disconnect(@ullable DisconnectCallback callback)334     public synchronized void disconnect(@Nullable DisconnectCallback callback) {
335         if (canDisconnect()) {
336             mCalledDisconnect = true;
337             mDisconnectCallback = callback;
338             mCallbackHandler.postDelayed(() -> {
339                 if (callback != null && mCalledDisconnect) {
340                     callback.onDisconnectResult(
341                             DisconnectCallback.DISCONNECT_STATUS_FAILURE_UNKNOWN);
342                 }
343             }, 10_000 /* delayMillis */);
344             mWifiManager.disableEphemeralNetwork(mFqdn);
345             mWifiManager.disconnect();
346         }
347     }
348 
349     @Override
canForget()350     public synchronized boolean canForget() {
351         return !isSuggestion() && mPasspointConfig != null;
352     }
353 
354     @Override
forget(@ullable ForgetCallback callback)355     public synchronized void forget(@Nullable ForgetCallback callback) {
356         if (!canForget()) {
357             return;
358         }
359 
360         mForgetCallback = callback;
361         mWifiManager.removePasspointConfiguration(mPasspointConfig.getHomeSp().getFqdn());
362         new ForgetActionListener().onSuccess();
363     }
364 
365     @Override
366     @MeteredChoice
getMeteredChoice()367     public synchronized int getMeteredChoice() {
368         if (mMeteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED) {
369             return METERED_CHOICE_METERED;
370         } else if (mMeteredOverride == WifiConfiguration.METERED_OVERRIDE_NOT_METERED) {
371             return METERED_CHOICE_UNMETERED;
372         }
373         return METERED_CHOICE_AUTO;
374     }
375 
376     @Override
canSetMeteredChoice()377     public synchronized boolean canSetMeteredChoice() {
378         return !isSuggestion() && mPasspointConfig != null;
379     }
380 
381     @Override
setMeteredChoice(int meteredChoice)382     public synchronized void setMeteredChoice(int meteredChoice) {
383         if (mPasspointConfig == null || !canSetMeteredChoice()) {
384             return;
385         }
386 
387         switch (meteredChoice) {
388             case METERED_CHOICE_AUTO:
389                 mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_NONE;
390                 break;
391             case METERED_CHOICE_METERED:
392                 mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED;
393                 break;
394             case METERED_CHOICE_UNMETERED:
395                 mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_NOT_METERED;
396                 break;
397             default:
398                 // Do nothing.
399                 return;
400         }
401         mWifiManager.setPasspointMeteredOverride(mPasspointConfig.getHomeSp().getFqdn(),
402                 mMeteredOverride);
403     }
404 
405     @Override
canSetPrivacy()406     public synchronized boolean canSetPrivacy() {
407         return !isSuggestion() && mPasspointConfig != null;
408     }
409 
410     @Override
411     @Privacy
getPrivacy()412     public synchronized int getPrivacy() {
413         if (mPasspointConfig == null) {
414             return PRIVACY_RANDOMIZED_MAC;
415         }
416 
417         return mPasspointConfig.isMacRandomizationEnabled()
418                 ? PRIVACY_RANDOMIZED_MAC : PRIVACY_DEVICE_MAC;
419     }
420 
421     @Override
setPrivacy(int privacy)422     public synchronized void setPrivacy(int privacy) {
423         if (mPasspointConfig == null || !canSetPrivacy()) {
424             return;
425         }
426 
427         mWifiManager.setMacRandomizationSettingPasspointEnabled(
428                 mPasspointConfig.getHomeSp().getFqdn(),
429                 privacy == PRIVACY_DEVICE_MAC ? false : true);
430     }
431 
432     @Override
isAutoJoinEnabled()433     public synchronized boolean isAutoJoinEnabled() {
434         // Suggestion network; use WifiConfig instead
435         if (mPasspointConfig != null) {
436             return mPasspointConfig.isAutojoinEnabled();
437         }
438         if (mWifiConfig != null) {
439             return mWifiConfig.allowAutojoin;
440         }
441         return false;
442     }
443 
444     @Override
canSetAutoJoinEnabled()445     public synchronized boolean canSetAutoJoinEnabled() {
446         return mPasspointConfig != null || mWifiConfig != null;
447     }
448 
449     @Override
setAutoJoinEnabled(boolean enabled)450     public synchronized void setAutoJoinEnabled(boolean enabled) {
451         if (mPasspointConfig != null) {
452             mWifiManager.allowAutojoinPasspoint(mPasspointConfig.getHomeSp().getFqdn(), enabled);
453         } else if (mWifiConfig != null) {
454             mWifiManager.allowAutojoin(mWifiConfig.networkId, enabled);
455         }
456     }
457 
458     @Override
getSecurityString(boolean concise)459     public String getSecurityString(boolean concise) {
460         return mContext.getString(R.string.wifitrackerlib_wifi_security_passpoint);
461     }
462 
463     @Override
getStandardString()464     public synchronized String getStandardString() {
465         if (mWifiInfo != null) {
466             return Utils.getStandardString(mContext, mWifiInfo.getWifiStandard());
467         }
468         if (!mCurrentHomeScanResults.isEmpty()) {
469             return Utils.getStandardString(
470                     mContext, mCurrentHomeScanResults.get(0).getWifiStandard());
471         }
472         if (!mCurrentRoamingScanResults.isEmpty()) {
473             return Utils.getStandardString(
474                     mContext, mCurrentRoamingScanResults.get(0).getWifiStandard());
475         }
476         return "";
477     }
478 
479     @Override
getBandString()480     public synchronized String getBandString() {
481         if (mWifiInfo != null) {
482             return Utils.wifiInfoToBandString(mContext, mWifiInfo);
483         }
484         if (!mCurrentHomeScanResults.isEmpty()) {
485             return Utils.frequencyToBandString(mContext, mCurrentHomeScanResults.get(0).frequency);
486         }
487         if (!mCurrentRoamingScanResults.isEmpty()) {
488             return Utils.frequencyToBandString(
489                     mContext, mCurrentRoamingScanResults.get(0).frequency);
490         }
491         return "";
492     }
493 
494     @Override
isExpired()495     public synchronized boolean isExpired() {
496         if (mSubscriptionExpirationTimeInMillis <= 0) {
497             // Expiration time not specified.
498             return false;
499         } else {
500             return System.currentTimeMillis() >= mSubscriptionExpirationTimeInMillis;
501         }
502     }
503 
504     @WorkerThread
updatePasspointConfig(@ullable PasspointConfiguration passpointConfig)505     synchronized void updatePasspointConfig(@Nullable PasspointConfiguration passpointConfig) {
506         mPasspointConfig = passpointConfig;
507         if (mPasspointConfig != null) {
508             mSubscriptionExpirationTimeInMillis =
509                     passpointConfig.getSubscriptionExpirationTimeMillis();
510             mMeteredOverride = passpointConfig.getMeteredOverride();
511         }
512         notifyOnUpdated();
513     }
514 
515     @WorkerThread
updateScanResultInfo(@ullable WifiConfiguration wifiConfig, @Nullable List<ScanResult> homeScanResults, @Nullable List<ScanResult> roamingScanResults)516     synchronized void updateScanResultInfo(@Nullable WifiConfiguration wifiConfig,
517             @Nullable List<ScanResult> homeScanResults,
518             @Nullable List<ScanResult> roamingScanResults)
519             throws IllegalArgumentException {
520         mWifiConfig = wifiConfig;
521         mCurrentHomeScanResults.clear();
522         mCurrentRoamingScanResults.clear();
523         if (homeScanResults != null) {
524             mCurrentHomeScanResults.addAll(homeScanResults);
525         }
526         if (roamingScanResults != null) {
527             mCurrentRoamingScanResults.addAll(roamingScanResults);
528         }
529         if (mWifiConfig != null) {
530             List<ScanResult> currentScanResults = new ArrayList<>();
531             if (homeScanResults != null && !homeScanResults.isEmpty()) {
532                 currentScanResults.addAll(homeScanResults);
533             } else if (roamingScanResults != null && !roamingScanResults.isEmpty()) {
534                 currentScanResults.addAll(roamingScanResults);
535             }
536             ScanResult bestScanResult = getBestScanResultByLevel(currentScanResults);
537             if (bestScanResult != null) {
538                 mWifiConfig.SSID = "\"" + bestScanResult.SSID + "\"";
539             }
540             if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
541                 mLevel = bestScanResult != null
542                         ? mWifiManager.calculateSignalLevel(bestScanResult.level)
543                         : WIFI_LEVEL_UNREACHABLE;
544             }
545         } else {
546             mLevel = WIFI_LEVEL_UNREACHABLE;
547         }
548         notifyOnUpdated();
549     }
550 
551     @Override
updateSecurityTypes()552     protected synchronized void updateSecurityTypes() {
553         if (mWifiInfo != null) {
554             final int wifiInfoSecurity = mWifiInfo.getCurrentSecurityType();
555             if (wifiInfoSecurity != SECURITY_TYPE_UNKNOWN) {
556                 mTargetSecurityTypes = Collections.singletonList(wifiInfoSecurity);
557                 return;
558             }
559         }
560     }
561 
562     @WorkerThread
563     @Override
connectionInfoMatches(@onNull WifiInfo wifiInfo)564     protected boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo) {
565         if (!wifiInfo.isPasspointAp()) {
566             return false;
567         }
568 
569         if (BuildCompat.isAtLeastV() && NonSdkApiWrapper.isAndroidVWifiApiEnabled()) {
570             return TextUtils.equals(mUniqueId, wifiInfo.getPasspointUniqueId());
571         }
572 
573         // Match with FQDN if WifiInfo doesn't support returning the uniqueID.
574         return TextUtils.equals(wifiInfo.getPasspointFqdn(), mFqdn);
575     }
576 
577     @WorkerThread
578     @Override
onNetworkCapabilitiesChanged( @onNull Network network, @NonNull NetworkCapabilities capabilities)579     synchronized void onNetworkCapabilitiesChanged(
580             @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
581         super.onNetworkCapabilitiesChanged(network, capabilities);
582 
583         // Auto-open an available captive portal if the user manually connected to this network.
584         if (canSignIn() && mShouldAutoOpenCaptivePortal) {
585             mShouldAutoOpenCaptivePortal = false;
586             signIn(null /* callback */);
587         }
588     }
589 
590     @NonNull
uniqueIdToPasspointWifiEntryKey(@onNull String uniqueId)591     static String uniqueIdToPasspointWifiEntryKey(@NonNull String uniqueId) {
592         checkNotNull(uniqueId, "Cannot create key with null unique id!");
593         return KEY_PREFIX + uniqueId;
594     }
595 
596     @Override
getScanResultDescription()597     protected String getScanResultDescription() {
598         // TODO(b/70983952): Fill this method in.
599         return "";
600     }
601 
602     @Override
getNetworkSelectionDescription()603     synchronized String getNetworkSelectionDescription() {
604         return Utils.getNetworkSelectionDescription(mWifiConfig);
605     }
606 
607     /** Pass a reference to a matching OsuWifiEntry for expiration handling */
setOsuWifiEntry(OsuWifiEntry osuWifiEntry)608     synchronized void setOsuWifiEntry(OsuWifiEntry osuWifiEntry) {
609         mOsuWifiEntry = osuWifiEntry;
610         if (mOsuWifiEntry != null) {
611             mOsuWifiEntry.setListener(this);
612         }
613     }
614 
615     /** Callback for updates to the linked OsuWifiEntry */
616     @Override
onUpdated()617     public void onUpdated() {
618         notifyOnUpdated();
619     }
620 
621     @Override
canSignIn()622     public synchronized boolean canSignIn() {
623         return mNetwork != null
624                 && mNetworkCapabilities != null
625                 && mNetworkCapabilities.hasCapability(
626                 NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
627     }
628 
629     @Override
signIn(@ullable SignInCallback callback)630     public void signIn(@Nullable SignInCallback callback) {
631         if (canSignIn()) {
632             NonSdkApiWrapper.startCaptivePortalApp(
633                     mContext.getSystemService(ConnectivityManager.class), mNetwork);
634         }
635     }
636 
637     /** Get the PasspointConfiguration instance of the entry. */
getPasspointConfig()638     public PasspointConfiguration getPasspointConfig() {
639         return mPasspointConfig;
640     }
641 
642     @Override
toString()643     public String toString() {
644         StringJoiner sj = new StringJoiner("][", "[", "]");
645         sj.add("FQDN:" + mFqdn);
646         sj.add("FriendlyName:" + mFriendlyName);
647         if (mPasspointConfig != null) {
648             sj.add("UniqueId:" + mPasspointConfig.getUniqueId());
649         } else if (mWifiConfig != null) {
650             sj.add("UniqueId:" + mWifiConfig.getKey());
651         }
652         return super.toString() + sj;
653     }
654 }
655