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