• 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 androidx.core.util.Preconditions.checkNotNull;
20 
21 import static com.android.wifitrackerlib.PasspointWifiEntry.uniqueIdToPasspointWifiEntryKey;
22 import static com.android.wifitrackerlib.StandardWifiEntry.ScanResultKey;
23 import static com.android.wifitrackerlib.StandardWifiEntry.StandardWifiEntryKey;
24 
25 import static java.util.stream.Collectors.toMap;
26 
27 import android.content.Context;
28 import android.content.Intent;
29 import android.net.ConnectivityDiagnosticsManager;
30 import android.net.ConnectivityManager;
31 import android.net.LinkProperties;
32 import android.net.Network;
33 import android.net.NetworkCapabilities;
34 import android.net.NetworkInfo;
35 import android.net.wifi.ScanResult;
36 import android.net.wifi.WifiConfiguration;
37 import android.net.wifi.WifiEnterpriseConfig;
38 import android.net.wifi.WifiInfo;
39 import android.net.wifi.WifiManager;
40 import android.net.wifi.hotspot2.PasspointConfiguration;
41 import android.os.Handler;
42 import android.text.TextUtils;
43 import android.util.ArrayMap;
44 import android.util.Log;
45 import android.util.Pair;
46 
47 import androidx.annotation.AnyThread;
48 import androidx.annotation.GuardedBy;
49 import androidx.annotation.MainThread;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.annotation.VisibleForTesting;
53 import androidx.annotation.WorkerThread;
54 import androidx.lifecycle.Lifecycle;
55 
56 import java.time.Clock;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.Collections;
60 import java.util.List;
61 import java.util.Map;
62 import java.util.Set;
63 import java.util.TreeSet;
64 import java.util.function.Function;
65 import java.util.stream.Collectors;
66 
67 /**
68  * Wi-Fi tracker that provides all Wi-Fi related data to the Saved Networks page.
69  *
70  * These include
71  * - List of WifiEntries for all saved networks, dynamically updated with ScanResults
72  * - List of WifiEntries for all saved subscriptions, dynamically updated with ScanResults
73  */
74 public class SavedNetworkTracker extends BaseWifiTracker {
75 
76     private static final String TAG = "SavedNetworkTracker";
77 
78     private final SavedNetworkTrackerCallback mListener;
79 
80     // Lock object for data returned by the public API
81     private final Object mLock = new Object();
82 
83     @GuardedBy("mLock") private final List<WifiEntry> mSavedWifiEntries = new ArrayList<>();
84     @GuardedBy("mLock") private final List<WifiEntry> mSubscriptionWifiEntries = new ArrayList<>();
85 
86     // Cache containing saved StandardWifiEntries. Must be accessed only by the worker thread.
87     private final List<StandardWifiEntry> mStandardWifiEntryCache = new ArrayList<>();
88     // Cache containing saved PasspointWifiEntries. Must be accessed only by the worker thread.
89     private final Map<String, PasspointWifiEntry> mPasspointWifiEntryCache = new ArrayMap<>();
90 
SavedNetworkTracker(@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 SavedNetworkTrackerCallback listener)91     public SavedNetworkTracker(@NonNull Lifecycle lifecycle, @NonNull Context context,
92             @NonNull WifiManager wifiManager,
93             @NonNull ConnectivityManager connectivityManager,
94             @NonNull Handler mainHandler,
95             @NonNull Handler workerHandler,
96             @NonNull Clock clock,
97             long maxScanAgeMillis,
98             long scanIntervalMillis,
99             @Nullable SavedNetworkTrackerCallback listener) {
100         this(new WifiTrackerInjector(context), lifecycle, context, wifiManager, connectivityManager,
101                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener);
102     }
103 
104     @VisibleForTesting
SavedNetworkTracker( @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 SavedNetworkTrackerCallback listener)105     SavedNetworkTracker(
106             @NonNull WifiTrackerInjector injector,
107             @NonNull Lifecycle lifecycle,
108             @NonNull Context context,
109             @NonNull WifiManager wifiManager,
110             @NonNull ConnectivityManager connectivityManager,
111             @NonNull Handler mainHandler,
112             @NonNull Handler workerHandler,
113             @NonNull Clock clock,
114             long maxScanAgeMillis,
115             long scanIntervalMillis,
116             @Nullable SavedNetworkTrackerCallback listener) {
117         super(injector, lifecycle, context, wifiManager, connectivityManager,
118                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener,
119                 TAG);
120         mListener = listener;
121     }
122 
123     /**
124      * Returns a list of WifiEntries for all saved networks. If a network is in range, the
125      * corresponding WifiEntry will be updated with live ScanResult data.
126      * @return
127      */
128     @AnyThread
129     @NonNull
getSavedWifiEntries()130     public List<WifiEntry> getSavedWifiEntries() {
131         synchronized (mLock) {
132             return new ArrayList<>(mSavedWifiEntries);
133         }
134     }
135 
136     /**
137      * Returns a list of WifiEntries for all saved subscriptions. If a subscription network is in
138      * range, the corresponding WifiEntry will be updated with live ScanResult data.
139      * @return
140      */
141     @AnyThread
142     @NonNull
getSubscriptionWifiEntries()143     public List<WifiEntry> getSubscriptionWifiEntries() {
144         synchronized (mLock) {
145             return new ArrayList<>(mSubscriptionWifiEntries);
146         }
147     }
148 
149     /** Check whether or not CA certificate is set.
150      *
151      * WifiEnterpriseConfig::hasCaCertificate() is only available
152      * after API level 33.
153      */
hasCaCertificate(WifiEnterpriseConfig ec)154     private static boolean hasCaCertificate(WifiEnterpriseConfig ec) {
155         if (ec.getCaCertificateAliases() != null) return true;
156         if (ec.getCaCertificates() != null) return true;
157         if (!TextUtils.isEmpty(ec.getCaPath())) return true;
158         return false;
159     }
160 
isCertificateUsedByConfiguration( WifiConfiguration config, String certAlias)161     private static boolean isCertificateUsedByConfiguration(
162             WifiConfiguration config, String certAlias) {
163         if (TextUtils.isEmpty(certAlias)) return false;
164         if (config == null) return false;
165         if (config.enterpriseConfig == null) return false;
166         WifiEnterpriseConfig ec = config.enterpriseConfig;
167         if (!ec.isEapMethodServerCertUsed()) return false;
168         if (!hasCaCertificate(ec) && TextUtils.isEmpty(ec.getClientCertificateAlias())) {
169             return false;
170         }
171 
172         String[] aliases = ec.getCaCertificateAliases();
173         if (aliases != null) {
174             for (String s: aliases) {
175                 if (!TextUtils.isEmpty(s) && certAlias.equals(s)) {
176                     return true;
177                 }
178             }
179         }
180         String clientAlias = ec.getClientCertificateAlias();
181         if (!TextUtils.isEmpty(clientAlias)
182                 && certAlias.equals(clientAlias)) {
183             return true;
184         }
185         return false;
186     }
187 
188     /**
189      * Check whether or not a certifiate is required by saved networks or network suggestions.
190      */
191     @AnyThread
isCertificateRequired(String certAlias)192     public boolean isCertificateRequired(String certAlias) {
193         // Configurations from Wi-Fi Network Suggestion
194         List<WifiConfiguration> configurations = mWifiManager.getNetworkSuggestions()
195                 .stream().map(s -> s.getWifiConfiguration())
196                 .collect(Collectors.toList());
197         // Configurations from regular Wi-Fi configurations.
198         configurations.addAll(mWifiManager.getConfiguredNetworks());
199 
200         return configurations.stream()
201                 .anyMatch(c -> isCertificateUsedByConfiguration(c, certAlias));
202     }
203 
204     /**
205      * Returns a list of network names which requires the certificate alias.
206      *
207      * @return a list of network names.
208      */
209     @AnyThread
210     @NonNull
getCertificateRequesterNames(String certAlias)211     public List<String> getCertificateRequesterNames(String certAlias) {
212         // Configurations from Wi-Fi Network Suggestion
213         List<WifiConfiguration> configurations = mWifiManager.getNetworkSuggestions()
214                 .stream().map(s -> s.getWifiConfiguration())
215                 .collect(Collectors.toList());
216         // Configurations from regular Wi-Fi configurations.
217         configurations.addAll(mWifiManager.getConfiguredNetworks());
218 
219         return configurations.stream()
220                 .filter(c -> isCertificateUsedByConfiguration(c, certAlias))
221                 .map(c -> c.SSID).collect(Collectors.toSet())
222                 .stream().collect(Collectors.toList());
223     }
224 
getAllWifiEntries()225     private List<WifiEntry> getAllWifiEntries() {
226         List<WifiEntry> allEntries = new ArrayList<>();
227         allEntries.addAll(mStandardWifiEntryCache);
228         allEntries.addAll(mPasspointWifiEntryCache.values());
229         return allEntries;
230     }
231 
232     @WorkerThread
233     @Override
handleOnStart()234     protected void handleOnStart() {
235         // Update configs and scans
236         updateStandardWifiEntryConfigs(mWifiManager.getConfiguredNetworks());
237         updatePasspointWifiEntryConfigs(mWifiManager.getPasspointConfigurations());
238         mScanResultUpdater.update(mWifiManager.getScanResults());
239         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
240 
241         // Trigger callbacks manually now to avoid waiting until the first calls to update state.
242         // Clear any stale connection info in case we missed any NetworkCallback.onLost() while in
243         // the stopped state, but don't notify the listener to avoid flicker from disconnected ->
244         // connected in case the network is still the same.
245         for (WifiEntry entry : getAllWifiEntries()) {
246             entry.clearConnectionInfo(false);
247         }
248         Network currentNetwork = mWifiManager.getCurrentNetwork();
249         if (currentNetwork != null) {
250             NetworkCapabilities networkCapabilities =
251                     mConnectivityManager.getNetworkCapabilities(currentNetwork);
252             if (networkCapabilities != null) {
253                 // getNetworkCapabilities(Network) obfuscates location info such as SSID and
254                 // networkId, so we need to set the WifiInfo directly from WifiManager.
255                 handleNetworkCapabilitiesChanged(currentNetwork,
256                         new NetworkCapabilities.Builder(networkCapabilities)
257                                 .setTransportInfo(mWifiManager.getConnectionInfo())
258                                 .build());
259             }
260             LinkProperties linkProperties = mConnectivityManager.getLinkProperties(currentNetwork);
261             if (linkProperties != null) {
262                 handleLinkPropertiesChanged(currentNetwork, linkProperties);
263             }
264         }
265         updateWifiEntries();
266     }
267 
268     @WorkerThread
269     @Override
handleWifiStateChangedAction()270     protected void handleWifiStateChangedAction() {
271         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
272         updateWifiEntries();
273     }
274 
275     @WorkerThread
276     @Override
handleScanResultsAvailableAction(@ullable Intent intent)277     protected void handleScanResultsAvailableAction(@Nullable Intent intent) {
278         checkNotNull(intent, "Intent cannot be null!");
279         conditionallyUpdateScanResults(intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED,
280                 true /* defaultValue */));
281         updateWifiEntries();
282     }
283 
284     @WorkerThread
285     @Override
handleConfiguredNetworksChangedAction(@ullable Intent intent)286     protected void handleConfiguredNetworksChangedAction(@Nullable Intent intent) {
287         checkNotNull(intent, "Intent cannot be null!");
288         updateStandardWifiEntryConfigs(mWifiManager.getConfiguredNetworks());
289         updatePasspointWifiEntryConfigs(mWifiManager.getPasspointConfigurations());
290         updateWifiEntries();
291     }
292 
293     @WorkerThread
294     @Override
handleNetworkStateChangedAction(@onNull Intent intent)295     protected void handleNetworkStateChangedAction(@NonNull Intent intent) {
296         WifiInfo primaryWifiInfo = mWifiManager.getConnectionInfo();
297         NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
298         if (primaryWifiInfo == null || networkInfo == null) {
299             return;
300         }
301         for (WifiEntry entry : getAllWifiEntries()) {
302             entry.onPrimaryWifiInfoChanged(primaryWifiInfo, networkInfo);
303         }
304     }
305 
306     @WorkerThread
307     @Override
handleLinkPropertiesChanged( @onNull Network network, @Nullable LinkProperties linkProperties)308     protected void handleLinkPropertiesChanged(
309             @NonNull Network network, @Nullable LinkProperties linkProperties) {
310         for (WifiEntry entry : getAllWifiEntries()) {
311             entry.updateLinkProperties(network, linkProperties);
312         }
313     }
314 
315     @WorkerThread
316     @Override
handleNetworkCapabilitiesChanged( @onNull Network network, @NonNull NetworkCapabilities capabilities)317     protected void handleNetworkCapabilitiesChanged(
318             @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
319         updateConnectionInfo(network, capabilities);
320         updateWifiEntries();
321     }
322 
323     @WorkerThread
324     @Override
handleNetworkLost(@onNull Network network)325     protected void handleNetworkLost(@NonNull Network network) {
326         for (WifiEntry entry : getAllWifiEntries()) {
327             entry.onNetworkLost(network);
328         }
329         updateWifiEntries();
330     }
331 
332     @WorkerThread
333     @Override
handleConnectivityReportAvailable( @onNull ConnectivityDiagnosticsManager.ConnectivityReport connectivityReport)334     protected void handleConnectivityReportAvailable(
335             @NonNull ConnectivityDiagnosticsManager.ConnectivityReport connectivityReport) {
336         for (WifiEntry entry : getAllWifiEntries()) {
337             entry.updateConnectivityReport(connectivityReport);
338         }
339     }
340 
341 
342     @WorkerThread
handleDefaultNetworkCapabilitiesChanged(@onNull Network network, @NonNull NetworkCapabilities networkCapabilities)343     protected void handleDefaultNetworkCapabilitiesChanged(@NonNull Network network,
344             @NonNull NetworkCapabilities networkCapabilities) {
345         for (WifiEntry entry : getAllWifiEntries()) {
346             entry.onDefaultNetworkCapabilitiesChanged(network, networkCapabilities);
347         }
348     }
349 
350     @WorkerThread
351     @Override
handleDefaultNetworkLost()352     protected void handleDefaultNetworkLost() {
353         for (WifiEntry entry : getAllWifiEntries()) {
354             entry.onDefaultNetworkLost();
355         }
356     }
357 
358     /**
359      * Update the list returned by {@link #getSavedWifiEntries()} and
360      * {@link #getSubscriptionWifiEntries()} with the current states of the entry caches.
361      */
updateWifiEntries()362     private void updateWifiEntries() {
363         synchronized (mLock) {
364             mSavedWifiEntries.clear();
365             mSavedWifiEntries.addAll(mStandardWifiEntryCache);
366             Collections.sort(mSavedWifiEntries, WifiEntry.TITLE_COMPARATOR);
367             mSubscriptionWifiEntries.clear();
368             mSubscriptionWifiEntries.addAll(mPasspointWifiEntryCache.values());
369             Collections.sort(mSubscriptionWifiEntries, WifiEntry.TITLE_COMPARATOR);
370             if (isVerboseLoggingEnabled()) {
371                 Log.v(TAG, "Updated SavedWifiEntries: "
372                         + Arrays.toString(mSavedWifiEntries.toArray()));
373                 Log.v(TAG, "Updated SubscriptionWifiEntries: "
374                         + Arrays.toString(mSubscriptionWifiEntries.toArray()));
375             }
376         }
377         notifyOnSavedWifiEntriesChanged();
378         notifyOnSubscriptionWifiEntriesChanged();
379     }
380 
updateStandardWifiEntryScans(@onNull List<ScanResult> scanResults)381     private void updateStandardWifiEntryScans(@NonNull List<ScanResult> scanResults) {
382         checkNotNull(scanResults, "Scan Result list should not be null!");
383 
384         // Group scans by StandardWifiEntry key
385         final Map<ScanResultKey, List<ScanResult>> scanResultsByKey = scanResults.stream()
386                 .collect(Collectors.groupingBy(StandardWifiEntry.ScanResultKey::new));
387 
388         // Iterate through current entries and update each entry's scan results
389         mStandardWifiEntryCache.forEach(entry -> {
390             // Update scan results if available, or set to null.
391             entry.updateScanResultInfo(
392                     scanResultsByKey.get(entry.getStandardWifiEntryKey().getScanResultKey()));
393         });
394     }
395 
updatePasspointWifiEntryScans(@onNull List<ScanResult> scanResults)396     private void updatePasspointWifiEntryScans(@NonNull List<ScanResult> scanResults) {
397         checkNotNull(scanResults, "Scan Result list should not be null!");
398 
399         Set<String> seenKeys = new TreeSet<>();
400         List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> matchingWifiConfigs =
401                 mWifiManager.getAllMatchingWifiConfigs(scanResults);
402         for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pair : matchingWifiConfigs) {
403             final WifiConfiguration wifiConfig = pair.first;
404             final String key = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
405             seenKeys.add(key);
406             // Skip in case we don't have a PasspointWifiEntry for the returned unique identifier.
407             if (!mPasspointWifiEntryCache.containsKey(key)) {
408                 continue;
409             }
410 
411             mPasspointWifiEntryCache.get(key).updateScanResultInfo(wifiConfig,
412                     pair.second.get(WifiManager.PASSPOINT_HOME_NETWORK),
413                     pair.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK));
414         }
415 
416         for (PasspointWifiEntry entry : mPasspointWifiEntryCache.values()) {
417             if (!seenKeys.contains(entry.getKey())) {
418                 // No AP in range; set scan results and connection config to null.
419                 entry.updateScanResultInfo(null /* wifiConfig */,
420                         null /* homeScanResults */,
421                         null /* roamingScanResults */);
422             }
423         }
424     }
425 
426     /**
427      * Conditionally updates the WifiEntry scan results based on the current wifi state and
428      * whether the last scan succeeded or not.
429      */
430     @WorkerThread
conditionallyUpdateScanResults(boolean lastScanSucceeded)431     private void conditionallyUpdateScanResults(boolean lastScanSucceeded) {
432         if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) {
433             updateStandardWifiEntryScans(Collections.emptyList());
434             updatePasspointWifiEntryScans(Collections.emptyList());
435             return;
436         }
437 
438         long scanAgeWindow = mMaxScanAgeMillis;
439         if (lastScanSucceeded) {
440             // Scan succeeded, cache new scans
441             mScanResultUpdater.update(mWifiManager.getScanResults());
442         } else {
443             // Scan failed, increase scan age window to prevent WifiEntry list from
444             // clearing prematurely.
445             scanAgeWindow = MAX_SCAN_AGE_FOR_FAILED_SCAN_MS;
446         }
447         List<ScanResult> currentScans = mScanResultUpdater.getScanResults(scanAgeWindow);
448         updateStandardWifiEntryScans(currentScans);
449         updatePasspointWifiEntryScans(currentScans);
450     }
451 
updateStandardWifiEntryConfigs(@onNull List<WifiConfiguration> configs)452     private void updateStandardWifiEntryConfigs(@NonNull List<WifiConfiguration> configs) {
453         checkNotNull(configs, "Config list should not be null!");
454 
455         // Group configs by StandardWifiEntry key
456         final Map<StandardWifiEntryKey, List<WifiConfiguration>> wifiConfigsByKey = configs.stream()
457                 .filter(config -> !config.carrierMerged)
458                 .collect(Collectors.groupingBy(StandardWifiEntryKey::new));
459 
460         // Iterate through current entries and update each entry's config
461         mStandardWifiEntryCache.removeIf(entry -> {
462             // Update config if available, or set to null (unsaved)
463             entry.updateConfig(wifiConfigsByKey.remove(entry.getStandardWifiEntryKey()));
464             // Entry is now unsaved, remove it.
465             return !entry.isSaved();
466         });
467 
468         // Create new entry for each unmatched config
469         for (StandardWifiEntryKey key : wifiConfigsByKey.keySet()) {
470             mStandardWifiEntryCache.add(new StandardWifiEntry(mInjector, mMainHandler,
471                     key, wifiConfigsByKey.get(key), null, mWifiManager,
472                     true /* forSavedNetworksPage */));
473         }
474     }
475 
476     @WorkerThread
updatePasspointWifiEntryConfigs(@onNull List<PasspointConfiguration> configs)477     private void updatePasspointWifiEntryConfigs(@NonNull List<PasspointConfiguration> configs) {
478         checkNotNull(configs, "Config list should not be null!");
479 
480         final Map<String, PasspointConfiguration> passpointConfigsByKey =
481                 configs.stream().collect(toMap(
482                         (config) -> uniqueIdToPasspointWifiEntryKey(config.getUniqueId()),
483                         Function.identity()));
484 
485         // Iterate through current entries and update each entry's config or remove if no config
486         // matches the entry anymore.
487         mPasspointWifiEntryCache.entrySet().removeIf((entry) -> {
488             final PasspointWifiEntry wifiEntry = entry.getValue();
489             final String key = wifiEntry.getKey();
490             final PasspointConfiguration cachedConfig = passpointConfigsByKey.remove(key);
491             if (cachedConfig != null) {
492                 wifiEntry.updatePasspointConfig(cachedConfig);
493                 return false;
494             } else {
495                 return true;
496             }
497         });
498 
499         // Create new entry for each unmatched config
500         for (String key : passpointConfigsByKey.keySet()) {
501             mPasspointWifiEntryCache.put(key,
502                     new PasspointWifiEntry(mInjector, mMainHandler,
503                             passpointConfigsByKey.get(key), mWifiManager,
504                             true /* forSavedNetworksPage */));
505         }
506     }
507 
508     /**
509      * Updates all matching WifiEntries with the given connection info.
510      * @param network Network for which the NetworkCapabilities have changed.
511      * @param capabilities NetworkCapabilities that have changed.
512      */
513     @WorkerThread
updateConnectionInfo( @onNull Network network, @NonNull NetworkCapabilities capabilities)514     private void updateConnectionInfo(
515             @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
516         for (WifiEntry entry : getAllWifiEntries()) {
517             entry.onNetworkCapabilitiesChanged(network, capabilities);
518         }
519     }
520 
521     /**
522      * Posts onSavedWifiEntriesChanged callback on the main thread.
523      */
524     @WorkerThread
notifyOnSavedWifiEntriesChanged()525     private void notifyOnSavedWifiEntriesChanged() {
526         if (mListener != null) {
527             mMainHandler.post(mListener::onSavedWifiEntriesChanged);
528         }
529     }
530 
531     /**
532      * Posts onSubscriptionWifiEntriesChanged callback on the main thread.
533      */
534     @WorkerThread
notifyOnSubscriptionWifiEntriesChanged()535     private void notifyOnSubscriptionWifiEntriesChanged() {
536         if (mListener != null) {
537             mMainHandler.post(mListener::onSubscriptionWifiEntriesChanged);
538         }
539     }
540 
541     /**
542      * Listener for changes to the list of saved and subscription WifiEntries
543      *
544      * These callbacks must be run on the MainThread.
545      */
546     public interface SavedNetworkTrackerCallback extends BaseWifiTracker.BaseWifiTrackerCallback {
547         /**
548          * Called when there are changes to
549          *      {@link #getSavedWifiEntries()}
550          */
551         @MainThread
onSavedWifiEntriesChanged()552         void onSavedWifiEntriesChanged();
553 
554         /**
555          * Called when there are changes to
556          *      {@link #getSubscriptionWifiEntries()}
557          */
558         @MainThread
onSubscriptionWifiEntriesChanged()559         void onSubscriptionWifiEntriesChanged();
560     }
561 }
562