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