• 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.os.Build.VERSION_CODES;
20 
21 import static androidx.core.util.Preconditions.checkNotNull;
22 
23 import static com.android.wifitrackerlib.OsuWifiEntry.osuProviderToOsuWifiEntryKey;
24 import static com.android.wifitrackerlib.PasspointWifiEntry.uniqueIdToPasspointWifiEntryKey;
25 import static com.android.wifitrackerlib.StandardWifiEntry.ScanResultKey;
26 import static com.android.wifitrackerlib.StandardWifiEntry.StandardWifiEntryKey;
27 import static com.android.wifitrackerlib.WifiEntry.CONNECTED_STATE_DISCONNECTED;
28 import static com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE;
29 
30 import static java.util.stream.Collectors.toList;
31 import static java.util.stream.Collectors.toMap;
32 
33 import android.annotation.TargetApi;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.net.ConnectivityDiagnosticsManager;
37 import android.net.ConnectivityManager;
38 import android.net.LinkProperties;
39 import android.net.Network;
40 import android.net.NetworkCapabilities;
41 import android.net.NetworkInfo;
42 import android.net.wifi.ScanResult;
43 import android.net.wifi.WifiConfiguration;
44 import android.net.wifi.WifiInfo;
45 import android.net.wifi.WifiManager;
46 import android.net.wifi.hotspot2.OsuProvider;
47 import android.net.wifi.hotspot2.PasspointConfiguration;
48 import android.net.wifi.sharedconnectivity.app.HotspotNetwork;
49 import android.net.wifi.sharedconnectivity.app.HotspotNetworkConnectionStatus;
50 import android.net.wifi.sharedconnectivity.app.KnownNetwork;
51 import android.net.wifi.sharedconnectivity.app.KnownNetworkConnectionStatus;
52 import android.os.Handler;
53 import android.telephony.SubscriptionManager;
54 import android.text.TextUtils;
55 import android.util.ArrayMap;
56 import android.util.ArraySet;
57 import android.util.Log;
58 import android.util.Pair;
59 import android.util.SparseArray;
60 
61 import androidx.annotation.AnyThread;
62 import androidx.annotation.GuardedBy;
63 import androidx.annotation.MainThread;
64 import androidx.annotation.NonNull;
65 import androidx.annotation.Nullable;
66 import androidx.annotation.VisibleForTesting;
67 import androidx.annotation.WorkerThread;
68 import androidx.core.os.BuildCompat;
69 import androidx.lifecycle.Lifecycle;
70 
71 import java.time.Clock;
72 import java.util.ArrayList;
73 import java.util.Collections;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77 import java.util.StringJoiner;
78 import java.util.TreeSet;
79 import java.util.function.Function;
80 import java.util.stream.Collectors;
81 
82 /**
83  * Wi-Fi tracker that provides all Wi-Fi related data to the Wi-Fi picker page.
84  *
85  * These include
86  * - The connected WifiEntry
87  * - List of all visible WifiEntries
88  * - Number of saved networks
89  * - Number of saved subscriptions
90  */
91 public class WifiPickerTracker extends BaseWifiTracker {
92 
93     private static final String TAG = "WifiPickerTracker";
94 
95     private final WifiPickerTrackerCallback mListener;
96 
97     // Lock object for data returned by the public API
98     private final Object mLock = new Object();
99     // List representing the return value of the getActiveWifiEntries() API
100     @GuardedBy("mLock")
101     @NonNull private final List<WifiEntry> mActiveWifiEntries = new ArrayList<>();
102     // List representing the return value of the getWifiEntries() API
103     @GuardedBy("mLock")
104     @NonNull private final List<WifiEntry> mWifiEntries = new ArrayList<>();
105     // NetworkRequestEntry representing a network that was connected through the NetworkRequest API
106     private NetworkRequestEntry mNetworkRequestEntry;
107 
108     // Cache containing saved WifiConfigurations mapped by StandardWifiEntry key
109     private final Map<StandardWifiEntryKey, List<WifiConfiguration>> mStandardWifiConfigCache =
110             new ArrayMap<>();
111     // Cache containing suggested WifiConfigurations mapped by StandardWifiEntry key
112     private final Map<StandardWifiEntryKey, List<WifiConfiguration>> mSuggestedConfigCache =
113             new ArrayMap<>();
114     // Cache containing network request WifiConfigurations mapped by StandardWifiEntry key.
115     private final ArrayMap<StandardWifiEntryKey, List<WifiConfiguration>>
116             mNetworkRequestConfigCache = new ArrayMap<>();
117     // Cache containing visible StandardWifiEntries. Must be accessed only by the worker thread.
118     private final List<StandardWifiEntry> mStandardWifiEntryCache = new ArrayList<>();
119     // Cache containing available suggested StandardWifiEntries. These entries may be already
120     // represented in mStandardWifiEntryCache, so filtering must be done before they are returned in
121     // getWifiEntry() and getConnectedWifiEntry().
122     private final List<StandardWifiEntry> mSuggestedWifiEntryCache = new ArrayList<>();
123     // Cache containing saved PasspointConfigurations mapped by PasspointWifiEntry key.
124     private final Map<String, PasspointConfiguration> mPasspointConfigCache = new ArrayMap<>();
125     // Cache containing Passpoint WifiConfigurations mapped by network id.
126     private final SparseArray<WifiConfiguration> mPasspointWifiConfigCache = new SparseArray<>();
127     // Cache containing visible PasspointWifiEntries. Must be accessed only by the worker thread.
128     private final Map<String, PasspointWifiEntry> mPasspointWifiEntryCache = new ArrayMap<>();
129     // Cache containing visible OsuWifiEntries. Must be accessed only by the worker thread.
130     private final Map<String, OsuWifiEntry> mOsuWifiEntryCache = new ArrayMap<>();
131 
132     private MergedCarrierEntry mMergedCarrierEntry;
133 
134     private int mNumSavedNetworks;
135 
136     private final List<KnownNetwork> mKnownNetworkDataCache = new ArrayList<>();
137     private final List<KnownNetworkEntry> mKnownNetworkEntryCache = new ArrayList<>();
138     private final List<HotspotNetwork> mHotspotNetworkDataCache = new ArrayList<>();
139     private final List<HotspotNetworkEntry> mHotspotNetworkEntryCache = new ArrayList<>();
140 
141     /**
142      * Constructor for WifiPickerTracker.
143      * @param lifecycle Lifecycle this is tied to for lifecycle callbacks.
144      * @param context Context for registering broadcast receiver and for resource strings.
145      * @param wifiManager Provides all Wi-Fi info.
146      * @param connectivityManager Provides network info.
147      * @param mainHandler Handler for processing listener callbacks.
148      * @param workerHandler Handler for processing all broadcasts and running the Scanner.
149      * @param clock Clock used for evaluating the age of scans
150      * @param maxScanAgeMillis Max age for tracked WifiEntries.
151      * @param scanIntervalMillis Interval between initiating scans.
152      * @param listener WifiTrackerCallback listening on changes to WifiPickerTracker data.
153      */
WifiPickerTracker(@onNull Lifecycle lifecycle, @NonNull Context context, @NonNull WifiManager wifiManager, @NonNull ConnectivityManager connectivityManager, @NonNull Handler mainHandler, @NonNull Handler workerHandler, @NonNull Clock clock, long maxScanAgeMillis, long scanIntervalMillis, @Nullable WifiPickerTrackerCallback listener)154     public WifiPickerTracker(@NonNull Lifecycle lifecycle, @NonNull Context context,
155             @NonNull WifiManager wifiManager,
156             @NonNull ConnectivityManager connectivityManager,
157             @NonNull Handler mainHandler,
158             @NonNull Handler workerHandler,
159             @NonNull Clock clock,
160             long maxScanAgeMillis,
161             long scanIntervalMillis,
162             @Nullable WifiPickerTrackerCallback listener) {
163         this(new WifiTrackerInjector(context), lifecycle, context, wifiManager, connectivityManager,
164                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener);
165     }
166 
167     @VisibleForTesting
WifiPickerTracker( @onNull WifiTrackerInjector injector, @NonNull Lifecycle lifecycle, @NonNull Context context, @NonNull WifiManager wifiManager, @NonNull ConnectivityManager connectivityManager, @NonNull Handler mainHandler, @NonNull Handler workerHandler, @NonNull Clock clock, long maxScanAgeMillis, long scanIntervalMillis, @Nullable WifiPickerTrackerCallback listener)168     WifiPickerTracker(
169             @NonNull WifiTrackerInjector injector,
170             @NonNull Lifecycle lifecycle,
171             @NonNull Context context,
172             @NonNull WifiManager wifiManager,
173             @NonNull ConnectivityManager connectivityManager,
174             @NonNull Handler mainHandler,
175             @NonNull Handler workerHandler,
176             @NonNull Clock clock,
177             long maxScanAgeMillis,
178             long scanIntervalMillis,
179             @Nullable WifiPickerTrackerCallback listener) {
180         super(injector, lifecycle, context, wifiManager, connectivityManager,
181                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener,
182                 TAG);
183         mListener = listener;
184     }
185 
186     /**
187      * Returns the WifiEntry representing the current primary connection.
188      */
189     @AnyThread
getConnectedWifiEntry()190     public @Nullable WifiEntry getConnectedWifiEntry() {
191         synchronized (mLock) {
192             if (mActiveWifiEntries.isEmpty()) {
193                 return null;
194             }
195             // Primary entry is sorted to be first.
196             WifiEntry primaryWifiEntry = mActiveWifiEntries.get(0);
197             if (!primaryWifiEntry.isPrimaryNetwork()) {
198                 return null;
199             }
200             return primaryWifiEntry;
201         }
202     }
203 
204     /**
205      * Returns a list of all connected/connecting Wi-Fi entries, including the primary and any
206      * secondary connections.
207      */
208     @AnyThread
getActiveWifiEntries()209     public @NonNull List<WifiEntry> getActiveWifiEntries() {
210         synchronized (mLock) {
211             return new ArrayList<>(mActiveWifiEntries);
212         }
213     }
214 
215     /**
216      * Returns a list of disconnected, in-range WifiEntries.
217      *
218      * The currently connected entry is omitted and may be accessed through
219      * {@link #getConnectedWifiEntry()}
220      */
221     @AnyThread
getWifiEntries()222     public @NonNull List<WifiEntry> getWifiEntries() {
223         synchronized (mLock) {
224             return new ArrayList<>(mWifiEntries);
225         }
226     }
227 
228     /**
229      * Returns the MergedCarrierEntry representing the active carrier subscription.
230      */
231     @AnyThread
getMergedCarrierEntry()232     public @Nullable MergedCarrierEntry getMergedCarrierEntry() {
233         if (!isInitialized() && mMergedCarrierEntry == null) {
234             // Settings currently relies on the MergedCarrierEntry being available before
235             // handleOnStart() is called in order to display the W+ toggle. Populate it here if
236             // we aren't initialized yet.
237             int subId = SubscriptionManager.getDefaultDataSubscriptionId();
238             if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
239                 mMergedCarrierEntry = new MergedCarrierEntry(mInjector, mWorkerHandler,
240                         mWifiManager, /* forSavedNetworksPage */ false, subId);
241             }
242         }
243         return mMergedCarrierEntry;
244     }
245 
246     /**
247      * Returns the number of saved networks.
248      */
249     @AnyThread
getNumSavedNetworks()250     public int getNumSavedNetworks() {
251         return mNumSavedNetworks;
252     }
253 
254     /**
255      * Returns the number of saved subscriptions.
256      */
257     @AnyThread
getNumSavedSubscriptions()258     public int getNumSavedSubscriptions() {
259         return mPasspointConfigCache.size();
260     }
261 
getAllWifiEntries()262     private List<WifiEntry> getAllWifiEntries() {
263         List<WifiEntry> allEntries = new ArrayList<>();
264         allEntries.addAll(mStandardWifiEntryCache);
265         allEntries.addAll(mSuggestedWifiEntryCache);
266         allEntries.addAll(mPasspointWifiEntryCache.values());
267         allEntries.addAll(mOsuWifiEntryCache.values());
268         if (mInjector.isSharedConnectivityFeatureEnabled()) {
269             allEntries.addAll(mKnownNetworkEntryCache);
270             allEntries.addAll(mHotspotNetworkEntryCache);
271         }
272         if (mNetworkRequestEntry != null) {
273             allEntries.add(mNetworkRequestEntry);
274         }
275         if (mMergedCarrierEntry != null) {
276             allEntries.add(mMergedCarrierEntry);
277         }
278         return allEntries;
279     }
280 
clearAllWifiEntries()281     private void clearAllWifiEntries() {
282         mStandardWifiEntryCache.clear();
283         mSuggestedWifiEntryCache.clear();
284         mPasspointWifiEntryCache.clear();
285         mOsuWifiEntryCache.clear();
286         if (mInjector.isSharedConnectivityFeatureEnabled()) {
287             mKnownNetworkEntryCache.clear();
288             mHotspotNetworkEntryCache.clear();
289         }
290         mNetworkRequestEntry = null;
291     }
292 
293     @WorkerThread
294     @Override
handleOnStart()295     protected void handleOnStart() {
296         // Remove stale WifiEntries remaining from the last onStop().
297         clearAllWifiEntries();
298 
299         // Update configs and scans
300         updateWifiConfigurations(mWifiManager.getPrivilegedConfiguredNetworks());
301         updatePasspointConfigurations(mWifiManager.getPasspointConfigurations());
302         mScanResultUpdater.update(mWifiManager.getScanResults());
303         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
304 
305         // Trigger callbacks manually now to avoid waiting until the first calls to update state.
306         handleDefaultSubscriptionChanged(SubscriptionManager.getDefaultDataSubscriptionId());
307         Network currentNetwork = mWifiManager.getCurrentNetwork();
308         if (currentNetwork != null) {
309             NetworkCapabilities networkCapabilities =
310                     mConnectivityManager.getNetworkCapabilities(currentNetwork);
311             if (networkCapabilities != null) {
312                 // getNetworkCapabilities(Network) obfuscates location info such as SSID and
313                 // networkId, so we need to set the WifiInfo directly from WifiManager.
314                 handleNetworkCapabilitiesChanged(currentNetwork,
315                         new NetworkCapabilities.Builder(networkCapabilities)
316                                 .setTransportInfo(mWifiManager.getConnectionInfo())
317                                 .build());
318             }
319             LinkProperties linkProperties = mConnectivityManager.getLinkProperties(currentNetwork);
320             if (linkProperties != null) {
321                 handleLinkPropertiesChanged(currentNetwork, linkProperties);
322             }
323         }
324         notifyOnNumSavedNetworksChanged();
325         notifyOnNumSavedSubscriptionsChanged();
326         updateWifiEntries();
327     }
328 
329     @WorkerThread
330     @Override
handleWifiStateChangedAction()331     protected void handleWifiStateChangedAction() {
332         if (getWifiState() == WifiManager.WIFI_STATE_DISABLED) {
333             clearAllWifiEntries();
334         }
335         updateWifiEntries();
336     }
337 
338     @WorkerThread
339     @Override
handleScanResultsAvailableAction(@onNull Intent intent)340     protected void handleScanResultsAvailableAction(@NonNull Intent intent) {
341         checkNotNull(intent, "Intent cannot be null!");
342         conditionallyUpdateScanResults(
343                 intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true));
344         updateWifiEntries();
345     }
346 
347     @WorkerThread
348     @Override
handleConfiguredNetworksChangedAction(@onNull Intent intent)349     protected void handleConfiguredNetworksChangedAction(@NonNull Intent intent) {
350         checkNotNull(intent, "Intent cannot be null!");
351 
352         processConfiguredNetworksChanged();
353     }
354 
355     @WorkerThread
356     /** All wifi entries and saved entries needs to be updated. */
processConfiguredNetworksChanged()357     protected void processConfiguredNetworksChanged() {
358         updateWifiConfigurations(mWifiManager.getPrivilegedConfiguredNetworks());
359         updatePasspointConfigurations(mWifiManager.getPasspointConfigurations());
360         // Update scans since config changes may result in different entries being shown.
361         final List<ScanResult> scanResults = mScanResultUpdater.getScanResults();
362         updateStandardWifiEntryScans(scanResults);
363         updateNetworkRequestEntryScans(scanResults);
364         updatePasspointWifiEntryScans(scanResults);
365         updateOsuWifiEntryScans(scanResults);
366         if (mInjector.isSharedConnectivityFeatureEnabled() && BuildCompat.isAtLeastU()) {
367             updateKnownNetworkEntryScans(scanResults);
368             // Updating the hotspot entries here makes the UI more reliable when switching pages or
369             // when toggling settings while the internet picker is shown.
370             updateHotspotNetworkEntries();
371         }
372         notifyOnNumSavedNetworksChanged();
373         notifyOnNumSavedSubscriptionsChanged();
374         updateWifiEntries();
375     }
376 
377     @WorkerThread
378     @Override
handleNetworkStateChangedAction(@onNull Intent intent)379     protected void handleNetworkStateChangedAction(@NonNull Intent intent) {
380         WifiInfo primaryWifiInfo = mWifiManager.getConnectionInfo();
381         NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
382         if (primaryWifiInfo != null) {
383             conditionallyCreateConnectedWifiEntry(primaryWifiInfo);
384         }
385         for (WifiEntry entry : getAllWifiEntries()) {
386             entry.onPrimaryWifiInfoChanged(primaryWifiInfo, networkInfo);
387         }
388         updateWifiEntries();
389     }
390 
391     @WorkerThread
392     @Override
handleRssiChangedAction(@onNull Intent intent)393     protected void handleRssiChangedAction(@NonNull Intent intent) {
394         // RSSI is available via the new WifiInfo object, which is used to populate the RSSI in the
395         // verbose summary.
396         WifiInfo primaryWifiInfo = mWifiManager.getConnectionInfo();
397         for (WifiEntry entry : getAllWifiEntries()) {
398             entry.onPrimaryWifiInfoChanged(primaryWifiInfo, null);
399         }
400     }
401 
402     @WorkerThread
403     @Override
handleLinkPropertiesChanged( @onNull Network network, @Nullable LinkProperties linkProperties)404     protected void handleLinkPropertiesChanged(
405             @NonNull Network network, @Nullable LinkProperties linkProperties) {
406         for (WifiEntry entry : getAllWifiEntries()) {
407             entry.updateLinkProperties(network, linkProperties);
408         }
409     }
410 
411     @WorkerThread
412     @Override
handleNetworkCapabilitiesChanged( @onNull Network network, @NonNull NetworkCapabilities capabilities)413     protected void handleNetworkCapabilitiesChanged(
414             @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
415         updateNetworkCapabilities(network, capabilities);
416         updateWifiEntries();
417     }
418 
419     @WorkerThread
420     @Override
handleNetworkLost(@onNull Network network)421     protected void handleNetworkLost(@NonNull Network network) {
422         for (WifiEntry entry : getAllWifiEntries()) {
423             entry.onNetworkLost(network);
424         }
425         if (mNetworkRequestEntry != null
426                 && mNetworkRequestEntry.getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
427             mNetworkRequestEntry = null;
428         }
429         updateWifiEntries();
430     }
431 
432     @WorkerThread
433     @Override
handleConnectivityReportAvailable( @onNull ConnectivityDiagnosticsManager.ConnectivityReport connectivityReport)434     protected void handleConnectivityReportAvailable(
435             @NonNull ConnectivityDiagnosticsManager.ConnectivityReport connectivityReport) {
436         for (WifiEntry entry : getAllWifiEntries()) {
437             entry.updateConnectivityReport(connectivityReport);
438         }
439     }
440 
441     @WorkerThread
handleDefaultNetworkCapabilitiesChanged(@onNull Network network, @NonNull NetworkCapabilities networkCapabilities)442     protected void handleDefaultNetworkCapabilitiesChanged(@NonNull Network network,
443             @NonNull NetworkCapabilities networkCapabilities) {
444         for (WifiEntry entry : getAllWifiEntries()) {
445             entry.onDefaultNetworkCapabilitiesChanged(network, networkCapabilities);
446         }
447     }
448 
449     @WorkerThread
450     @Override
handleDefaultNetworkLost()451     protected void handleDefaultNetworkLost() {
452         for (WifiEntry entry : getAllWifiEntries()) {
453             entry.onDefaultNetworkLost();
454         }
455     }
456 
457     @WorkerThread
458     @Override
handleDefaultSubscriptionChanged(int defaultSubId)459     protected void handleDefaultSubscriptionChanged(int defaultSubId) {
460         updateMergedCarrierEntry(defaultSubId);
461     }
462 
463     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
464     @WorkerThread
465     @Override
handleKnownNetworksUpdated(List<KnownNetwork> networks)466     protected void handleKnownNetworksUpdated(List<KnownNetwork> networks) {
467         if (mInjector.isSharedConnectivityFeatureEnabled()) {
468             mKnownNetworkDataCache.clear();
469             mKnownNetworkDataCache.addAll(networks);
470             updateKnownNetworkEntryScans(mScanResultUpdater.getScanResults());
471             updateWifiEntries();
472         }
473     }
474 
475     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
476     @WorkerThread
477     @Override
handleHotspotNetworksUpdated(List<HotspotNetwork> networks)478     protected void handleHotspotNetworksUpdated(List<HotspotNetwork> networks) {
479         if (mInjector.isSharedConnectivityFeatureEnabled()) {
480             mHotspotNetworkDataCache.clear();
481             mHotspotNetworkDataCache.addAll(networks);
482             updateHotspotNetworkEntries();
483             updateWifiEntries();
484         }
485     }
486     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
487     @WorkerThread
handleHotspotNetworkConnectionStatusChanged( @onNull HotspotNetworkConnectionStatus status)488     protected void handleHotspotNetworkConnectionStatusChanged(
489             @NonNull HotspotNetworkConnectionStatus status) {
490         mHotspotNetworkEntryCache.stream().filter(
491                 entry -> entry.getHotspotNetworkEntryKey().getDeviceId()
492                         == status.getHotspotNetwork().getDeviceId()).forEach(
493                                 entry -> entry.onConnectionStatusChanged(status.getStatus()));
494     }
495 
496     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
497     @WorkerThread
498     @Override
handleKnownNetworkConnectionStatusChanged( @onNull KnownNetworkConnectionStatus status)499     protected void handleKnownNetworkConnectionStatusChanged(
500             @NonNull KnownNetworkConnectionStatus status) {
501         final ScanResultKey key = new ScanResultKey(status.getKnownNetwork().getSsid(),
502                 status.getKnownNetwork().getSecurityTypes().stream().toList());
503         mKnownNetworkEntryCache.stream().filter(
504                 entry -> entry.getStandardWifiEntryKey().getScanResultKey().equals(key)).forEach(
505                         entry -> entry.onConnectionStatusChanged(status.getStatus()));
506     }
507 
508     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
509     @WorkerThread
510     @Override
handleServiceConnected()511     protected void handleServiceConnected() {
512         if (mInjector.isSharedConnectivityFeatureEnabled()) {
513             mKnownNetworkDataCache.clear();
514             mKnownNetworkDataCache.addAll(mSharedConnectivityManager.getKnownNetworks());
515             mHotspotNetworkDataCache.clear();
516             mHotspotNetworkDataCache.addAll(mSharedConnectivityManager.getHotspotNetworks());
517             updateKnownNetworkEntryScans(mScanResultUpdater.getScanResults());
518             updateHotspotNetworkEntries();
519             updateWifiEntries();
520         }
521     }
522 
523     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
524     @WorkerThread
525     @Override
handleServiceDisconnected()526     protected void handleServiceDisconnected() {
527         if (mInjector.isSharedConnectivityFeatureEnabled()) {
528             mKnownNetworkDataCache.clear();
529             mHotspotNetworkDataCache.clear();
530             mKnownNetworkEntryCache.clear();
531             mHotspotNetworkEntryCache.clear();
532             updateWifiEntries();
533         }
534     }
535 
536     /**
537      * Update the list returned by getWifiEntries() with the current states of the entry caches.
538      */
539     @WorkerThread
updateWifiEntries()540     protected void updateWifiEntries() {
541         synchronized (mLock) {
542             mActiveWifiEntries.clear();
543             mActiveWifiEntries.addAll(mStandardWifiEntryCache);
544             mActiveWifiEntries.addAll(mSuggestedWifiEntryCache);
545             mActiveWifiEntries.addAll(mPasspointWifiEntryCache.values());
546             if (mInjector.isSharedConnectivityFeatureEnabled()) {
547                 mActiveWifiEntries.addAll(mHotspotNetworkEntryCache);
548             }
549             if (mNetworkRequestEntry != null) {
550                 mActiveWifiEntries.add(mNetworkRequestEntry);
551             }
552             mActiveWifiEntries.removeIf(entry ->
553                     entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED);
554             Set<ScanResultKey> activeHotspotNetworkKeys = new ArraySet<>();
555             for (WifiEntry entry : mActiveWifiEntries) {
556                 if (entry instanceof HotspotNetworkEntry) {
557                     activeHotspotNetworkKeys.add(((HotspotNetworkEntry) entry)
558                             .getHotspotNetworkEntryKey().getScanResultKey());
559                 }
560             }
561             mActiveWifiEntries.removeIf(entry -> entry instanceof StandardWifiEntry
562                     && activeHotspotNetworkKeys.contains(
563                     ((StandardWifiEntry) entry).getStandardWifiEntryKey().getScanResultKey()));
564             mActiveWifiEntries.sort(WifiEntry.WIFI_PICKER_COMPARATOR);
565             mWifiEntries.clear();
566             final Set<ScanResultKey> scanResultKeysWithVisibleSuggestions =
567                     mSuggestedWifiEntryCache.stream()
568                             .filter(entry -> {
569                                 if (entry.isUserShareable()) return true;
570                                 synchronized (mLock) {
571                                     return mActiveWifiEntries.contains(entry);
572                                 }
573                             })
574                             .map(entry -> entry.getStandardWifiEntryKey().getScanResultKey())
575                             .collect(Collectors.toSet());
576             Set<String> passpointUtf8Ssids = new ArraySet<>();
577             for (PasspointWifiEntry passpointWifiEntry : mPasspointWifiEntryCache.values()) {
578                 passpointUtf8Ssids.addAll(passpointWifiEntry.getAllUtf8Ssids());
579             }
580             Set<ScanResultKey> knownNetworkKeys = new ArraySet<>();
581             for (KnownNetworkEntry knownNetworkEntry : mKnownNetworkEntryCache) {
582                 knownNetworkKeys.add(
583                         knownNetworkEntry.getStandardWifiEntryKey().getScanResultKey());
584             }
585             Set<ScanResultKey> hotspotNetworkKeys = new ArraySet<>();
586             for (HotspotNetworkEntry hotspotNetworkEntry : mHotspotNetworkEntryCache) {
587                 if (!hotspotNetworkEntry.getHotspotNetworkEntryKey().isVirtualEntry()) {
588                     hotspotNetworkKeys.add(
589                             hotspotNetworkEntry.getHotspotNetworkEntryKey().getScanResultKey());
590                 }
591             }
592             Set<ScanResultKey> savedEntryKeys = new ArraySet<>();
593             for (StandardWifiEntry entry : mStandardWifiEntryCache) {
594                 entry.updateAdminRestrictions();
595                 if (mActiveWifiEntries.contains(entry)) {
596                     continue;
597                 }
598                 if (!entry.isSaved()) {
599                     if (scanResultKeysWithVisibleSuggestions
600                             .contains(entry.getStandardWifiEntryKey().getScanResultKey())) {
601                         continue;
602                     }
603                     // Filter out any unsaved entries that are already provisioned with Passpoint
604                     if (passpointUtf8Ssids.contains(entry.getSsid())) {
605                         continue;
606                     }
607                     if (mInjector.isSharedConnectivityFeatureEnabled()) {
608                         // Filter out any unsaved entries that are matched with a KnownNetworkEntry
609                         if (knownNetworkKeys
610                                 .contains(entry.getStandardWifiEntryKey().getScanResultKey())) {
611                             continue;
612                         }
613                     }
614                 } else {
615                     // Create a set of saved entry keys
616                     savedEntryKeys.add(entry.getStandardWifiEntryKey().getScanResultKey());
617                 }
618                 if (mInjector.isSharedConnectivityFeatureEnabled()) {
619                     // Filter out any entries that are matched with a HotspotNetworkEntry
620                     if (hotspotNetworkKeys
621                             .contains(entry.getStandardWifiEntryKey().getScanResultKey())) {
622                         continue;
623                     }
624                 }
625                 mWifiEntries.add(entry);
626             }
627             mWifiEntries.addAll(mSuggestedWifiEntryCache.stream().filter(entry ->
628                     entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED
629                             && entry.isUserShareable()).collect(toList()));
630             mWifiEntries.addAll(mPasspointWifiEntryCache.values().stream().filter(entry ->
631                     entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED).collect(toList()));
632             mWifiEntries.addAll(mOsuWifiEntryCache.values().stream().filter(entry ->
633                     entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED
634                             && !entry.isAlreadyProvisioned()).collect(toList()));
635             mWifiEntries.addAll(getContextualWifiEntries().stream().filter(entry ->
636                     entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED).collect(toList()));
637             if (mInjector.isSharedConnectivityFeatureEnabled()) {
638                 mWifiEntries.addAll(mKnownNetworkEntryCache.stream().filter(entry ->
639                         (entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED)
640                                 && !(savedEntryKeys.contains(
641                                 entry.getStandardWifiEntryKey().getScanResultKey()))).collect(
642                         toList()));
643                 mWifiEntries.addAll(mHotspotNetworkEntryCache.stream().filter(entry ->
644                         entry.getConnectedState() == CONNECTED_STATE_DISCONNECTED).collect(
645                         toList()));
646             }
647             Collections.sort(mWifiEntries, WifiEntry.WIFI_PICKER_COMPARATOR);
648             if (isVerboseLoggingEnabled()) {
649                 StringJoiner entryLog = new StringJoiner("\n");
650                 int numEntries = mActiveWifiEntries.size() + mWifiEntries.size();
651                 int index = 1;
652                 for (WifiEntry entry : mActiveWifiEntries) {
653                     entryLog.add("Entry " + index + "/" + numEntries + ": " + entry);
654                     index++;
655                 }
656                 for (WifiEntry entry : mWifiEntries) {
657                     entryLog.add("Entry " + index + "/" + numEntries + ": " + entry);
658                     index++;
659                 }
660                 Log.v(TAG, entryLog.toString());
661                 Log.v(TAG, "MergedCarrierEntry: " + mMergedCarrierEntry);
662             }
663         }
664         notifyOnWifiEntriesChanged();
665     }
666 
667     /**
668      * Updates the MergedCarrierEntry returned by {@link #getMergedCarrierEntry()) with the current
669      * default data subscription ID, or sets it to null if not available.
670      */
671     @WorkerThread
updateMergedCarrierEntry(int subId)672     private void updateMergedCarrierEntry(int subId) {
673         if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
674             if (mMergedCarrierEntry == null) {
675                 return;
676             }
677             mMergedCarrierEntry = null;
678         } else {
679             if (mMergedCarrierEntry != null && subId == mMergedCarrierEntry.getSubscriptionId()) {
680                 return;
681             }
682             mMergedCarrierEntry = new MergedCarrierEntry(mInjector, mWorkerHandler, mWifiManager,
683                     /* forSavedNetworksPage */ false, subId);
684             Network currentNetwork = mWifiManager.getCurrentNetwork();
685             if (currentNetwork != null) {
686                 NetworkCapabilities networkCapabilities =
687                         mConnectivityManager.getNetworkCapabilities(currentNetwork);
688                 if (networkCapabilities != null) {
689                     // getNetworkCapabilities(Network) obfuscates location info such as SSID and
690                     // networkId, so we need to set the WifiInfo directly from WifiManager.
691                     mMergedCarrierEntry.onNetworkCapabilitiesChanged(currentNetwork,
692                             new NetworkCapabilities.Builder(networkCapabilities)
693                                     .setTransportInfo(mWifiManager.getConnectionInfo())
694                                     .build());
695                 }
696                 LinkProperties linkProperties =
697                         mConnectivityManager.getLinkProperties(currentNetwork);
698                 if (linkProperties != null) {
699                     mMergedCarrierEntry.updateLinkProperties(currentNetwork, linkProperties);
700                 }
701             }
702         }
703         notifyOnWifiEntriesChanged();
704     }
705 
706     /**
707      * Get the contextual WifiEntries added according to customized conditions.
708      */
getContextualWifiEntries()709     protected List<WifiEntry> getContextualWifiEntries() {
710         return Collections.emptyList();
711     }
712 
713     /**
714      * Update the contextual wifi entry according to customized conditions.
715      */
updateContextualWifiEntryScans(@onNull List<ScanResult> scanResults)716     protected void updateContextualWifiEntryScans(@NonNull List<ScanResult> scanResults) {
717         // do nothing
718     }
719 
720     /**
721      * Updates or removes scan results for the corresponding StandardWifiEntries.
722      * New entries will be created for scan results without an existing entry.
723      * Unreachable entries will be removed.
724      *
725      * @param scanResults List of valid scan results to convey as StandardWifiEntries
726      */
727     @WorkerThread
updateStandardWifiEntryScans(@onNull List<ScanResult> scanResults)728     private void updateStandardWifiEntryScans(@NonNull List<ScanResult> scanResults) {
729         checkNotNull(scanResults, "Scan Result list should not be null!");
730 
731         // Group scans by ScanResultKey key
732         final Map<ScanResultKey, List<ScanResult>> scanResultsByKey = scanResults.stream()
733                 .filter(scan -> !TextUtils.isEmpty(scan.SSID))
734                 .collect(Collectors.groupingBy(ScanResultKey::new));
735         final Set<ScanResultKey> newScanKeys = new ArraySet<>(scanResultsByKey.keySet());
736 
737         // Iterate through current entries and update each entry's scan results
738         mStandardWifiEntryCache.forEach(entry -> {
739             final ScanResultKey scanKey = entry.getStandardWifiEntryKey().getScanResultKey();
740             newScanKeys.remove(scanKey);
741             // Update scan results if available, or set to null.
742             entry.updateScanResultInfo(scanResultsByKey.get(scanKey));
743         });
744         // Create new StandardWifiEntry objects for each leftover group of scan results.
745         for (ScanResultKey scanKey: newScanKeys) {
746             final StandardWifiEntryKey entryKey =
747                     new StandardWifiEntryKey(scanKey, true /* isTargetingNewNetworks */);
748             final StandardWifiEntry newEntry = new StandardWifiEntry(mInjector,
749                     mMainHandler, entryKey, mStandardWifiConfigCache.get(entryKey),
750                     scanResultsByKey.get(scanKey), mWifiManager,
751                     false /* forSavedNetworksPage */);
752             mStandardWifiEntryCache.add(newEntry);
753         }
754 
755         // Remove any entry that is now unreachable due to no scans or unsupported
756         // security types.
757         mStandardWifiEntryCache.removeIf(
758                 entry -> entry.getLevel() == WIFI_LEVEL_UNREACHABLE);
759     }
760 
761     /**
762      * Updates or removes scan results for the corresponding StandardWifiEntries.
763      * New entries will be created for scan results without an existing entry.
764      * Unreachable entries will be removed.
765      *
766      * @param scanResults List of valid scan results to convey as StandardWifiEntries
767      */
768     @WorkerThread
updateSuggestedWifiEntryScans(@onNull List<ScanResult> scanResults)769     private void updateSuggestedWifiEntryScans(@NonNull List<ScanResult> scanResults) {
770         checkNotNull(scanResults, "Scan Result list should not be null!");
771 
772         // Get every ScanResultKey that is user shareable
773         final Set<StandardWifiEntryKey> userSharedEntryKeys =
774                 mWifiManager.getWifiConfigForMatchedNetworkSuggestionsSharedWithUser(scanResults)
775                         .stream()
776                         .map(StandardWifiEntryKey::new)
777                         .collect(Collectors.toSet());
778 
779         // Group scans by ScanResultKey key
780         final Map<ScanResultKey, List<ScanResult>> scanResultsByKey = scanResults.stream()
781                 .filter(scan -> !TextUtils.isEmpty(scan.SSID))
782                 .collect(Collectors.groupingBy(ScanResultKey::new));
783 
784         // Iterate through current entries and update each entry's scan results and shareability.
785         final Set<StandardWifiEntryKey> seenEntryKeys = new ArraySet<>();
786         mSuggestedWifiEntryCache.forEach(entry -> {
787             final StandardWifiEntryKey entryKey = entry.getStandardWifiEntryKey();
788             seenEntryKeys.add(entryKey);
789             // Update scan results if available, or set to null.
790             entry.updateScanResultInfo(scanResultsByKey.get(entryKey.getScanResultKey()));
791             entry.setUserShareable(userSharedEntryKeys.contains(entryKey));
792         });
793         // Create new StandardWifiEntry objects for each leftover config with scan results.
794         for (StandardWifiEntryKey entryKey : mSuggestedConfigCache.keySet()) {
795             final ScanResultKey scanKey = entryKey.getScanResultKey();
796             if (seenEntryKeys.contains(entryKey)
797                     || !scanResultsByKey.containsKey(scanKey)) {
798                 continue;
799             }
800             final StandardWifiEntry newEntry = new StandardWifiEntry(mInjector,
801                     mMainHandler, entryKey, mSuggestedConfigCache.get(entryKey),
802                     scanResultsByKey.get(scanKey), mWifiManager,
803                     false /* forSavedNetworksPage */);
804             newEntry.setUserShareable(userSharedEntryKeys.contains(entryKey));
805             mSuggestedWifiEntryCache.add(newEntry);
806         }
807 
808         // Remove any entry that is now unreachable due to no scans or unsupported
809         // security types.
810         mSuggestedWifiEntryCache.removeIf(entry -> entry.getLevel() == WIFI_LEVEL_UNREACHABLE);
811     }
812 
813     @WorkerThread
updatePasspointWifiEntryScans(@onNull List<ScanResult> scanResults)814     private void updatePasspointWifiEntryScans(@NonNull List<ScanResult> scanResults) {
815         checkNotNull(scanResults, "Scan Result list should not be null!");
816 
817         Set<String> seenKeys = new TreeSet<>();
818         List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> matchingWifiConfigs =
819                 mWifiManager.getAllMatchingWifiConfigs(scanResults);
820 
821         for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pair : matchingWifiConfigs) {
822             final WifiConfiguration wifiConfig = pair.first;
823             final List<ScanResult> homeScans =
824                     pair.second.get(WifiManager.PASSPOINT_HOME_NETWORK);
825             final List<ScanResult> roamingScans =
826                     pair.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK);
827             final String key = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
828             seenKeys.add(key);
829 
830             // Create PasspointWifiEntry if one doesn't exist for the seen key yet.
831             if (!mPasspointWifiEntryCache.containsKey(key)) {
832                 if (wifiConfig.fromWifiNetworkSuggestion) {
833                     mPasspointWifiEntryCache.put(key, new PasspointWifiEntry(mInjector, mContext,
834                             mMainHandler, wifiConfig, mWifiManager,
835                             false /* forSavedNetworksPage */));
836                 } else if (mPasspointConfigCache.containsKey(key)) {
837                     mPasspointWifiEntryCache.put(key, new PasspointWifiEntry(mInjector,
838                             mMainHandler, mPasspointConfigCache.get(key), mWifiManager,
839                             false /* forSavedNetworksPage */));
840                 } else {
841                     // Failed to find PasspointConfig for a provisioned Passpoint network
842                     continue;
843                 }
844             }
845             mPasspointWifiEntryCache.get(key).updateScanResultInfo(wifiConfig,
846                     homeScans, roamingScans);
847         }
848 
849         // Remove entries that are now unreachable
850         mPasspointWifiEntryCache.entrySet()
851                 .removeIf(entry -> entry.getValue().getLevel() == WIFI_LEVEL_UNREACHABLE
852                         || (!seenKeys.contains(entry.getKey()))
853                         && entry.getValue().getConnectedState() == CONNECTED_STATE_DISCONNECTED);
854     }
855 
856     @WorkerThread
updateOsuWifiEntryScans(@onNull List<ScanResult> scanResults)857     private void updateOsuWifiEntryScans(@NonNull List<ScanResult> scanResults) {
858         checkNotNull(scanResults, "Scan Result list should not be null!");
859 
860         Map<OsuProvider, List<ScanResult>> osuProviderToScans =
861                 mWifiManager.getMatchingOsuProviders(scanResults);
862         Map<OsuProvider, PasspointConfiguration> osuProviderToPasspointConfig =
863                 mWifiManager.getMatchingPasspointConfigsForOsuProviders(
864                         osuProviderToScans.keySet());
865         // Update each OsuWifiEntry with new scans (or empty scans).
866         for (OsuWifiEntry entry : mOsuWifiEntryCache.values()) {
867             entry.updateScanResultInfo(osuProviderToScans.remove(entry.getOsuProvider()));
868         }
869 
870         // Create a new entry for each OsuProvider not already matched to an OsuWifiEntry
871         for (OsuProvider provider : osuProviderToScans.keySet()) {
872             OsuWifiEntry newEntry = new OsuWifiEntry(mInjector, mMainHandler, provider,
873                     mWifiManager, false /* forSavedNetworksPage */);
874             newEntry.updateScanResultInfo(osuProviderToScans.get(provider));
875             mOsuWifiEntryCache.put(osuProviderToOsuWifiEntryKey(provider), newEntry);
876         }
877 
878         // Pass a reference of each OsuWifiEntry to any matching provisioned PasspointWifiEntries
879         // for expiration handling.
880         mOsuWifiEntryCache.values().forEach(osuEntry -> {
881             PasspointConfiguration provisionedConfig =
882                     osuProviderToPasspointConfig.get(osuEntry.getOsuProvider());
883             if (provisionedConfig == null) {
884                 osuEntry.setAlreadyProvisioned(false);
885                 return;
886             }
887             osuEntry.setAlreadyProvisioned(true);
888             PasspointWifiEntry provisionedEntry = mPasspointWifiEntryCache.get(
889                     uniqueIdToPasspointWifiEntryKey(provisionedConfig.getUniqueId()));
890             if (provisionedEntry == null) {
891                 return;
892             }
893             provisionedEntry.setOsuWifiEntry(osuEntry);
894         });
895 
896         // Remove entries that are now unreachable
897         mOsuWifiEntryCache.entrySet()
898                 .removeIf(entry -> entry.getValue().getLevel() == WIFI_LEVEL_UNREACHABLE);
899     }
900 
901     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
902     @WorkerThread
updateKnownNetworkEntryScans(@onNull List<ScanResult> scanResults)903     private void updateKnownNetworkEntryScans(@NonNull List<ScanResult> scanResults) {
904         checkNotNull(scanResults, "Scan Result list should not be null!");
905 
906         // Group scans by ScanResultKey key
907         final Map<ScanResultKey, List<ScanResult>> scanResultsByKey = scanResults.stream()
908                 .filter(scan -> !TextUtils.isEmpty(scan.SSID))
909                 .collect(Collectors.groupingBy(ScanResultKey::new));
910 
911         // Create a map of KnownNetwork data by ScanResultKey
912         final Map<ScanResultKey, KnownNetwork> knownNetworkDataByKey =
913                 mKnownNetworkDataCache.stream().collect(Collectors.toMap(
914                         data -> new ScanResultKey(data.getSsid(),
915                                 new ArrayList<>(data.getSecurityTypes())),
916                         data -> data,
917                         (data1, data2) -> {
918                             Log.e(TAG,
919                                     "Encountered duplicate key data in "
920                                             + "updateKnownNetworkEntryScans");
921                             return data1; // When duplicate data is encountered, use first one.
922                         }));
923 
924         // Remove entries not in latest data set from service
925         mKnownNetworkEntryCache.removeIf(entry -> !knownNetworkDataByKey.keySet().contains(
926                 entry.getStandardWifiEntryKey().getScanResultKey()));
927 
928         // Create set of ScanResultKeys for known networks from service that are included in scan
929         final Set<ScanResultKey> newScanKeys = knownNetworkDataByKey.keySet().stream().filter(
930                 scanResultsByKey::containsKey).collect(Collectors.toSet());
931 
932         // Iterate through current entries and update each entry's scan results
933         mKnownNetworkEntryCache.forEach(entry -> {
934             final ScanResultKey scanKey = entry.getStandardWifiEntryKey().getScanResultKey();
935             newScanKeys.remove(scanKey);
936             // Update scan results if available, or set to null.
937             entry.updateScanResultInfo(scanResultsByKey.get(scanKey));
938         });
939 
940         // Get network and capabilities if new network entries are being created
941         Network network = null;
942         NetworkCapabilities capabilities = null;
943         if (!newScanKeys.isEmpty()) {
944             network = mWifiManager.getCurrentNetwork();
945             if (network != null) {
946                 capabilities = mConnectivityManager.getNetworkCapabilities(network);
947                 if (capabilities != null) {
948                     // getNetworkCapabilities(Network) obfuscates location info such as SSID and
949                     // networkId, so we need to set the WifiInfo directly from WifiManager.
950                     capabilities = new NetworkCapabilities.Builder(capabilities).setTransportInfo(
951                             mWifiManager.getConnectionInfo()).build();
952                 }
953             }
954         }
955 
956         // Create new KnownNetworkEntry objects for each leftover group of scan results.
957         for (ScanResultKey scanKey : newScanKeys) {
958             final StandardWifiEntryKey entryKey =
959                     new StandardWifiEntryKey(scanKey, true /* isTargetingNewNetworks */);
960             final KnownNetworkEntry newEntry = new KnownNetworkEntry(mInjector,
961                     mMainHandler, entryKey, null /* configs */,
962                     scanResultsByKey.get(scanKey), mWifiManager,
963                     mSharedConnectivityManager, knownNetworkDataByKey.get(scanKey));
964             if (network != null && capabilities != null) {
965                 newEntry.onNetworkCapabilitiesChanged(network, capabilities);
966             }
967             mKnownNetworkEntryCache.add(newEntry);
968         }
969 
970         // Remove any entry that is now unreachable due to no scans or unsupported
971         // security types.
972         mKnownNetworkEntryCache.removeIf(
973                 entry -> entry.getLevel() == WIFI_LEVEL_UNREACHABLE);
974     }
975 
976     @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
977     @WorkerThread
updateHotspotNetworkEntries()978     private void updateHotspotNetworkEntries() {
979         // Map HotspotNetwork data by deviceID
980         final Map<Long, HotspotNetwork> hotspotNetworkDataById =
981                 mHotspotNetworkDataCache.stream().collect(Collectors.toMap(
982                         HotspotNetwork::getDeviceId,
983                         data -> data,
984                         (data1, data2) -> {
985                             Log.e(TAG,
986                                     "Encountered duplicate key data in "
987                                             + "updateHotspotNetworkEntries");
988                             return data1; // When duplicate data is encountered, use first one.
989                         }));
990         final Set<Long> newDeviceIds = new ArraySet<>(hotspotNetworkDataById.keySet());
991 
992         // Remove entries not in latest data set from service
993         mHotspotNetworkEntryCache.removeIf(
994                 entry -> !newDeviceIds.contains(entry.getHotspotNetworkEntryKey().getDeviceId()));
995 
996         // Iterate through entries and update HotspotNetwork data
997         mHotspotNetworkEntryCache.forEach(entry -> {
998             final Long deviceId = entry.getHotspotNetworkEntryKey().getDeviceId();
999             newDeviceIds.remove(deviceId);
1000             entry.updateHotspotNetworkData(hotspotNetworkDataById.get(deviceId));
1001         });
1002 
1003         // Get network and capabilities if new network entries are being created
1004         Network network = null;
1005         NetworkCapabilities capabilities = null;
1006         if (!newDeviceIds.isEmpty()) {
1007             network = mWifiManager.getCurrentNetwork();
1008             if (network != null) {
1009                 capabilities = mConnectivityManager.getNetworkCapabilities(network);
1010                 if (capabilities != null) {
1011                     // getNetworkCapabilities(Network) obfuscates location info such as SSID and
1012                     // networkId, so we need to set the WifiInfo directly from WifiManager.
1013                     capabilities = new NetworkCapabilities.Builder(capabilities).setTransportInfo(
1014                             mWifiManager.getConnectionInfo()).build();
1015                 }
1016             }
1017         }
1018 
1019         // Create new HotspotNetworkEntry objects for each new device ID
1020         for (Long deviceId : newDeviceIds) {
1021             final HotspotNetworkEntry newEntry = new HotspotNetworkEntry(mInjector, mContext,
1022                     mMainHandler, mWifiManager, mSharedConnectivityManager,
1023                     hotspotNetworkDataById.get(deviceId));
1024             if (network != null && capabilities != null) {
1025                 newEntry.onNetworkCapabilitiesChanged(network, capabilities);
1026             }
1027             mHotspotNetworkEntryCache.add(newEntry);
1028         }
1029     }
1030 
1031     @WorkerThread
updateNetworkRequestEntryScans(@onNull List<ScanResult> scanResults)1032     private void updateNetworkRequestEntryScans(@NonNull List<ScanResult> scanResults) {
1033         checkNotNull(scanResults, "Scan Result list should not be null!");
1034         if (mNetworkRequestEntry == null) {
1035             return;
1036         }
1037 
1038         final ScanResultKey scanKey =
1039                 mNetworkRequestEntry.getStandardWifiEntryKey().getScanResultKey();
1040         List<ScanResult> matchedScans = scanResults.stream()
1041                 .filter(scan -> scanKey.equals(new ScanResultKey(scan)))
1042                 .collect(toList());
1043         mNetworkRequestEntry.updateScanResultInfo(matchedScans);
1044     }
1045 
1046     /**
1047      * Conditionally updates the WifiEntry scan results based on the current wifi state and
1048      * whether the last scan succeeded or not.
1049      */
1050     @WorkerThread
conditionallyUpdateScanResults(boolean lastScanSucceeded)1051     private void conditionallyUpdateScanResults(boolean lastScanSucceeded) {
1052         if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) {
1053             updateStandardWifiEntryScans(Collections.emptyList());
1054             updateSuggestedWifiEntryScans(Collections.emptyList());
1055             updatePasspointWifiEntryScans(Collections.emptyList());
1056             updateOsuWifiEntryScans(Collections.emptyList());
1057             if (mInjector.isSharedConnectivityFeatureEnabled() && BuildCompat.isAtLeastU()) {
1058                 mKnownNetworkEntryCache.clear();
1059                 mHotspotNetworkEntryCache.clear();
1060             }
1061             updateNetworkRequestEntryScans(Collections.emptyList());
1062             updateContextualWifiEntryScans(Collections.emptyList());
1063             return;
1064         }
1065 
1066         long scanAgeWindow = mMaxScanAgeMillis;
1067         if (lastScanSucceeded) {
1068             // Scan succeeded, cache new scans
1069             mScanResultUpdater.update(mWifiManager.getScanResults());
1070         } else {
1071             // Scan failed, increase scan age window to prevent WifiEntry list from
1072             // clearing prematurely.
1073             scanAgeWindow += mScanIntervalMillis;
1074         }
1075 
1076         List<ScanResult> scanResults = mScanResultUpdater.getScanResults(scanAgeWindow);
1077         updateStandardWifiEntryScans(scanResults);
1078         updateSuggestedWifiEntryScans(scanResults);
1079         updatePasspointWifiEntryScans(scanResults);
1080         updateOsuWifiEntryScans(scanResults);
1081         if (mInjector.isSharedConnectivityFeatureEnabled() && BuildCompat.isAtLeastU()) {
1082             updateKnownNetworkEntryScans(scanResults);
1083             // Updating the hotspot entries here makes the UI more reliable when switching pages or
1084             // when toggling settings while the internet picker is shown.
1085             updateHotspotNetworkEntries();
1086         }
1087         updateNetworkRequestEntryScans(scanResults);
1088         updateContextualWifiEntryScans(scanResults);
1089     }
1090 
1091     /**
1092      * Updates the WifiConfiguration caches for saved/ephemeral/suggested networks and updates the
1093      * corresponding WifiEntries with the new configs.
1094      *
1095      * @param configs List of all saved/ephemeral/suggested WifiConfigurations
1096      */
1097     @WorkerThread
updateWifiConfigurations(@onNull List<WifiConfiguration> configs)1098     private void updateWifiConfigurations(@NonNull List<WifiConfiguration> configs) {
1099         checkNotNull(configs, "Config list should not be null!");
1100         mStandardWifiConfigCache.clear();
1101         mSuggestedConfigCache.clear();
1102         mNetworkRequestConfigCache.clear();
1103         for (WifiConfiguration config : configs) {
1104             if (config.carrierMerged) {
1105                 continue;
1106             }
1107             StandardWifiEntryKey standardWifiEntryKey =
1108                     new StandardWifiEntryKey(config, true /* isTargetingNewNetworks */);
1109             if (config.isPasspoint()) {
1110                 mPasspointWifiConfigCache.put(config.networkId, config);
1111             } else if (config.fromWifiNetworkSuggestion) {
1112                 if (!mSuggestedConfigCache.containsKey(standardWifiEntryKey)) {
1113                     mSuggestedConfigCache.put(standardWifiEntryKey, new ArrayList<>());
1114                 }
1115                 mSuggestedConfigCache.get(standardWifiEntryKey).add(config);
1116             } else if (config.fromWifiNetworkSpecifier) {
1117                 if (!mNetworkRequestConfigCache.containsKey(standardWifiEntryKey)) {
1118                     mNetworkRequestConfigCache.put(standardWifiEntryKey, new ArrayList<>());
1119                 }
1120                 mNetworkRequestConfigCache.get(standardWifiEntryKey).add(config);
1121             } else {
1122                 if (!mStandardWifiConfigCache.containsKey(standardWifiEntryKey)) {
1123                     mStandardWifiConfigCache.put(standardWifiEntryKey, new ArrayList<>());
1124                 }
1125                 mStandardWifiConfigCache.get(standardWifiEntryKey).add(config);
1126             }
1127         }
1128         mNumSavedNetworks = (int) mStandardWifiConfigCache.values().stream()
1129                 .flatMap(List::stream)
1130                 .filter(config -> !config.isEphemeral())
1131                 .map(config -> config.networkId)
1132                 .distinct()
1133                 .count();
1134 
1135         // Iterate through current entries and update each entry's config
1136         mStandardWifiEntryCache.forEach(entry ->
1137                 entry.updateConfig(mStandardWifiConfigCache.get(entry.getStandardWifiEntryKey())));
1138 
1139         // Iterate through current suggestion entries and update each entry's config
1140         mSuggestedWifiEntryCache.removeIf(entry -> {
1141             entry.updateConfig(mSuggestedConfigCache.get(entry.getStandardWifiEntryKey()));
1142             // Remove if the suggestion does not have a config anymore.
1143             return !entry.isSuggestion();
1144         });
1145         // Update suggestion scans to make sure we mark which suggestions are user-shareable.
1146         updateSuggestedWifiEntryScans(mScanResultUpdater.getScanResults());
1147 
1148         if (mNetworkRequestEntry != null) {
1149             mNetworkRequestEntry.updateConfig(
1150                     mNetworkRequestConfigCache.get(mNetworkRequestEntry.getStandardWifiEntryKey()));
1151         }
1152     }
1153 
1154     @WorkerThread
updatePasspointConfigurations(@onNull List<PasspointConfiguration> configs)1155     private void updatePasspointConfigurations(@NonNull List<PasspointConfiguration> configs) {
1156         checkNotNull(configs, "Config list should not be null!");
1157         mPasspointConfigCache.clear();
1158         mPasspointConfigCache.putAll(configs.stream().collect(
1159                 toMap(config -> uniqueIdToPasspointWifiEntryKey(
1160                         config.getUniqueId()), Function.identity())));
1161 
1162         // Iterate through current entries and update each entry's config or remove if no config
1163         // matches the entry anymore.
1164         mPasspointWifiEntryCache.entrySet().removeIf((entry) -> {
1165             final PasspointWifiEntry wifiEntry = entry.getValue();
1166             final String key = wifiEntry.getKey();
1167             wifiEntry.updatePasspointConfig(mPasspointConfigCache.get(key));
1168             return !wifiEntry.isSubscription() && !wifiEntry.isSuggestion();
1169         });
1170     }
1171 
1172     /**
1173      * Updates all matching WifiEntries with the given network capabilities. If there are
1174      * currently no matching WifiEntries, then a new WifiEntry will be created for the capabilities.
1175      * @param network Network for which the NetworkCapabilities have changed.
1176      * @param capabilities NetworkCapabilities that have changed.
1177      */
1178     @WorkerThread
updateNetworkCapabilities( @onNull Network network, @NonNull NetworkCapabilities capabilities)1179     private void updateNetworkCapabilities(
1180             @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
1181         if (mStandardWifiConfigCache.size()
1182                 + mSuggestedConfigCache.size() + mPasspointWifiConfigCache.size()
1183                 + mNetworkRequestConfigCache.size() == 0) {
1184             // We're connected but don't have any configured networks, so fetch the list of configs
1185             // again. This can happen when we fetch the configured networks after SSR, but the Wifi
1186             // thread times out waiting for driver restart and returns an empty list of networks.
1187             updateWifiConfigurations(mWifiManager.getPrivilegedConfiguredNetworks());
1188         }
1189         // Create a WifiEntry for the current connection if there are no scan results yet.
1190         conditionallyCreateConnectedWifiEntry(Utils.getWifiInfo(capabilities));
1191         for (WifiEntry entry : getAllWifiEntries()) {
1192             entry.onNetworkCapabilitiesChanged(network, capabilities);
1193         }
1194     }
1195 
conditionallyCreateConnectedWifiEntry(@onNull WifiInfo wifiInfo)1196     private void conditionallyCreateConnectedWifiEntry(@NonNull WifiInfo wifiInfo) {
1197         conditionallyCreateConnectedStandardWifiEntry(wifiInfo);
1198         conditionallyCreateConnectedSuggestedWifiEntry(wifiInfo);
1199         conditionallyCreateConnectedPasspointWifiEntry(wifiInfo);
1200         conditionallyCreateConnectedNetworkRequestEntry(wifiInfo);
1201     }
1202 
1203     /**
1204      * Updates the connection info of the current NetworkRequestEntry. A new NetworkRequestEntry is
1205      * created if there is no existing entry, or the existing entry doesn't match WifiInfo.
1206      */
1207     @WorkerThread
conditionallyCreateConnectedNetworkRequestEntry(@onNull WifiInfo wifiInfo)1208     private void conditionallyCreateConnectedNetworkRequestEntry(@NonNull WifiInfo wifiInfo) {
1209         final List<WifiConfiguration> matchingConfigs = new ArrayList<>();
1210 
1211         if (wifiInfo != null) {
1212             for (int i = 0; i < mNetworkRequestConfigCache.size(); i++) {
1213                 final List<WifiConfiguration> configs = mNetworkRequestConfigCache.valueAt(i);
1214                 if (!configs.isEmpty() && configs.get(0).networkId == wifiInfo.getNetworkId()) {
1215                     matchingConfigs.addAll(configs);
1216                     break;
1217                 }
1218             }
1219         }
1220         if (matchingConfigs.isEmpty()) {
1221             return;
1222         }
1223 
1224         // WifiInfo matches a request config, create a NetworkRequestEntry or update the existing.
1225         final StandardWifiEntryKey entryKey = new StandardWifiEntryKey(matchingConfigs.get(0));
1226         if (mNetworkRequestEntry == null
1227                 || !mNetworkRequestEntry.getStandardWifiEntryKey().equals(entryKey)) {
1228             mNetworkRequestEntry = new NetworkRequestEntry(mInjector, mMainHandler,
1229                     entryKey, mWifiManager, false /* forSavedNetworksPage */);
1230             mNetworkRequestEntry.updateConfig(matchingConfigs);
1231             updateNetworkRequestEntryScans(mScanResultUpdater.getScanResults());
1232         }
1233     }
1234 
1235     /**
1236      * If the given network is a standard Wi-Fi network and there are no scan results for this
1237      * network yet, create and cache a new StandardWifiEntry for it.
1238      */
1239     @WorkerThread
conditionallyCreateConnectedStandardWifiEntry(@onNull WifiInfo wifiInfo)1240     private void conditionallyCreateConnectedStandardWifiEntry(@NonNull WifiInfo wifiInfo) {
1241         if (wifiInfo == null || wifiInfo.isPasspointAp() || wifiInfo.isOsuAp()) {
1242             return;
1243         }
1244 
1245         final int connectedNetId = wifiInfo.getNetworkId();
1246         for (List<WifiConfiguration> configs : mStandardWifiConfigCache.values()) {
1247             // List of configs match as long as one of them matches the connected network ID.
1248             if (configs.stream()
1249                     .map(config -> config.networkId)
1250                     .filter(networkId -> networkId == connectedNetId)
1251                     .count() == 0) {
1252                 continue;
1253             }
1254             final StandardWifiEntryKey entryKey =
1255                     new StandardWifiEntryKey(configs.get(0), true /* isTargetingNewNetworks */);
1256             for (StandardWifiEntry existingEntry : mStandardWifiEntryCache) {
1257                 if (entryKey.equals(existingEntry.getStandardWifiEntryKey())) {
1258                     return;
1259                 }
1260             }
1261             final StandardWifiEntry connectedEntry =
1262                     new StandardWifiEntry(mInjector, mMainHandler, entryKey, configs,
1263                             null, mWifiManager, false /* forSavedNetworksPage */);
1264             mStandardWifiEntryCache.add(connectedEntry);
1265             return;
1266         }
1267     }
1268 
1269     /**
1270      * If the given network is a suggestion network and there are no scan results for this network
1271      * yet, create and cache a new StandardWifiEntry for it.
1272      */
1273     @WorkerThread
conditionallyCreateConnectedSuggestedWifiEntry(@onNull WifiInfo wifiInfo)1274     private void conditionallyCreateConnectedSuggestedWifiEntry(@NonNull WifiInfo wifiInfo) {
1275         if (wifiInfo == null || wifiInfo.isPasspointAp() || wifiInfo.isOsuAp()) {
1276             return;
1277         }
1278         final int connectedNetId = wifiInfo.getNetworkId();
1279         for (List<WifiConfiguration> configs : mSuggestedConfigCache.values()) {
1280             if (configs.isEmpty() || configs.get(0).networkId != connectedNetId) {
1281                 continue;
1282             }
1283             final StandardWifiEntryKey entryKey =
1284                     new StandardWifiEntryKey(configs.get(0), true /* isTargetingNewNetworks */);
1285             for (StandardWifiEntry existingEntry : mSuggestedWifiEntryCache) {
1286                 if (entryKey.equals(existingEntry.getStandardWifiEntryKey())) {
1287                     return;
1288                 }
1289             }
1290             final StandardWifiEntry connectedEntry =
1291                     new StandardWifiEntry(mInjector, mMainHandler, entryKey, configs,
1292                             null, mWifiManager, false /* forSavedNetworksPage */);
1293             mSuggestedWifiEntryCache.add(connectedEntry);
1294             return;
1295         }
1296     }
1297 
1298     /**
1299      * If the given network is a Passpoint network and there are no scan results for this network
1300      * yet, create and cache a new StandardWifiEntry for it.
1301      */
1302     @WorkerThread
conditionallyCreateConnectedPasspointWifiEntry(@onNull WifiInfo wifiInfo)1303     private void conditionallyCreateConnectedPasspointWifiEntry(@NonNull WifiInfo wifiInfo) {
1304         if (wifiInfo == null || !wifiInfo.isPasspointAp()) {
1305             return;
1306         }
1307 
1308         WifiConfiguration cachedWifiConfig = mPasspointWifiConfigCache.get(wifiInfo.getNetworkId());
1309         if (cachedWifiConfig == null) {
1310             return;
1311         }
1312         final String key = uniqueIdToPasspointWifiEntryKey(cachedWifiConfig.getKey());
1313         if (mPasspointWifiEntryCache.containsKey(key)) {
1314             // Entry already exists, skip creating a new one.
1315             return;
1316         }
1317         PasspointConfiguration passpointConfig = mPasspointConfigCache.get(
1318                 uniqueIdToPasspointWifiEntryKey(cachedWifiConfig.getKey()));
1319         PasspointWifiEntry connectedEntry;
1320         if (passpointConfig != null) {
1321             connectedEntry = new PasspointWifiEntry(mInjector, mMainHandler,
1322                     passpointConfig, mWifiManager,
1323                     false /* forSavedNetworksPage */);
1324         } else {
1325             // Suggested PasspointWifiEntry without a corresponding PasspointConfiguration
1326             connectedEntry = new PasspointWifiEntry(mInjector, mContext, mMainHandler,
1327                     cachedWifiConfig, mWifiManager,
1328                     false /* forSavedNetworksPage */);
1329         }
1330         mPasspointWifiEntryCache.put(connectedEntry.getKey(), connectedEntry);
1331     }
1332 
1333     /**
1334      * Posts onWifiEntryChanged callback on the main thread.
1335      */
1336     @WorkerThread
notifyOnWifiEntriesChanged()1337     private void notifyOnWifiEntriesChanged() {
1338         if (mListener != null) {
1339             mMainHandler.post(mListener::onWifiEntriesChanged);
1340         }
1341     }
1342 
1343     /**
1344      * Posts onNumSavedNetworksChanged callback on the main thread.
1345      */
1346     @WorkerThread
notifyOnNumSavedNetworksChanged()1347     private void notifyOnNumSavedNetworksChanged() {
1348         if (mListener != null) {
1349             mMainHandler.post(mListener::onNumSavedNetworksChanged);
1350         }
1351     }
1352 
1353     /**
1354      * Posts onNumSavedSubscriptionsChanged callback on the main thread.
1355      */
1356     @WorkerThread
notifyOnNumSavedSubscriptionsChanged()1357     private void notifyOnNumSavedSubscriptionsChanged() {
1358         if (mListener != null) {
1359             mMainHandler.post(mListener::onNumSavedSubscriptionsChanged);
1360         }
1361     }
1362 
1363     /**
1364      * Listener for changes to the list of visible WifiEntries as well as the number of saved
1365      * networks and subscriptions.
1366      *
1367      * These callbacks must be run on the MainThread.
1368      */
1369     public interface WifiPickerTrackerCallback extends BaseWifiTracker.BaseWifiTrackerCallback {
1370         /**
1371          * Called when there are changes to
1372          *      {@link #getConnectedWifiEntry()}
1373          *      {@link #getWifiEntries()}
1374          *      {@link #getMergedCarrierEntry()}
1375          */
1376         @MainThread
onWifiEntriesChanged()1377         void onWifiEntriesChanged();
1378 
1379         /**
1380          * Called when there are changes to
1381          *      {@link #getNumSavedNetworks()}
1382          */
1383         @MainThread
onNumSavedNetworksChanged()1384         void onNumSavedNetworksChanged();
1385 
1386         /**
1387          * Called when there are changes to
1388          *      {@link #getNumSavedSubscriptions()}
1389          */
1390         @MainThread
onNumSavedSubscriptionsChanged()1391         void onNumSavedSubscriptionsChanged();
1392     }
1393 }
1394