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