1 /* 2 * Copyright (C) 2021 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.tv.settings.library.network; 18 19 import android.annotation.AnyThread; 20 import android.annotation.MainThread; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.net.ConnectivityManager; 26 import android.net.Network; 27 import android.net.NetworkCapabilities; 28 import android.net.NetworkInfo; 29 import android.net.NetworkKey; 30 import android.net.NetworkRequest; 31 import android.net.NetworkScoreManager; 32 import android.net.ScoredNetwork; 33 import android.net.wifi.ScanResult; 34 import android.net.wifi.WifiConfiguration; 35 import android.net.wifi.WifiInfo; 36 import android.net.wifi.WifiManager; 37 import android.net.wifi.WifiNetworkScoreCache; 38 import android.net.wifi.WifiNetworkScoreCache.CacheListener; 39 import android.net.wifi.hotspot2.OsuProvider; 40 import android.os.Handler; 41 import android.os.HandlerThread; 42 import android.os.Message; 43 import android.os.Process; 44 import android.os.SystemClock; 45 import android.provider.Settings; 46 import android.text.format.DateUtils; 47 import android.util.ArrayMap; 48 import android.util.ArraySet; 49 import android.util.Log; 50 import android.util.Pair; 51 52 import androidx.annotation.GuardedBy; 53 import androidx.annotation.NonNull; 54 import androidx.annotation.VisibleForTesting; 55 56 import com.android.tv.settings.library.util.ThreadUtils; 57 import com.android.tv.settings.library.util.lifecycle.Lifecycle; 58 import com.android.tv.settings.library.util.lifecycle.LifecycleObserver; 59 import com.android.tv.settings.library.util.lifecycle.events.OnDestroy; 60 import com.android.tv.settings.library.util.lifecycle.events.OnStart; 61 import com.android.tv.settings.library.util.lifecycle.events.OnStop; 62 63 import java.io.PrintWriter; 64 import java.util.ArrayList; 65 import java.util.Collection; 66 import java.util.Collections; 67 import java.util.HashMap; 68 import java.util.Iterator; 69 import java.util.List; 70 import java.util.ListIterator; 71 import java.util.Map; 72 import java.util.Optional; 73 import java.util.Set; 74 import java.util.concurrent.atomic.AtomicBoolean; 75 import java.util.stream.Collectors; 76 77 /** 78 * Tracks saved or available wifi networks and their state. 79 * 80 * @deprecated WifiTracker/AccessPoint is no longer supported, and will be removed in a future 81 * release. Clients that need a dynamic list of available wifi networks should migrate to one of the 82 * newer tracker classes, 83 * {@link com.android.wifitrackerlib.WifiPickerTracker}, 84 * {@link com.android.wifitrackerlib.SavedNetworkTracker}, 85 * {@link com.android.wifitrackerlib.NetworkDetailsTracker}, 86 * in conjunction with {@link com.android.wifitrackerlib.WifiEntry} to represent each wifi network. 87 */ 88 @Deprecated 89 public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy { 90 /** 91 * Default maximum age in millis of cached scored networks in 92 * {@link .AccessPoint#mScoredNetworkCache} to be used for speed label generation. 93 */ 94 private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS; 95 96 /** Maximum age of scan results to hold onto while actively scanning. **/ 97 @VisibleForTesting 98 static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000; 99 100 private static final String TAG = "WifiTracker"; 101 DBG()102 private static final boolean DBG() { 103 return Log.isLoggable(TAG, Log.DEBUG); 104 } 105 isVerboseLoggingEnabled()106 private static boolean isVerboseLoggingEnabled() { 107 return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE); 108 } 109 110 /** 111 * Verbose logging flag set thru developer debugging options and used so as to assist with 112 * in-the-field WiFi connectivity debugging. 113 * 114 * <p>{@link #isVerboseLoggingEnabled()} should be read rather than referencing this value 115 * directly, to ensure adb TAG level verbose settings are respected. 116 */ 117 public static boolean sVerboseLogging; 118 119 // TODO: Allow control of this? 120 // Combo scans can take 5-6s to complete - set to 10s. 121 private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; 122 123 private final Context mContext; 124 private final WifiManager mWifiManager; 125 private final IntentFilter mFilter; 126 private final ConnectivityManager mConnectivityManager; 127 private final NetworkRequest mNetworkRequest; 128 private final AtomicBoolean mConnected = new AtomicBoolean(false); 129 private final WifiTracker.WifiListenerExecutor mListener; 130 @VisibleForTesting 131 Handler mWorkHandler; 132 private HandlerThread mWorkThread; 133 134 private WifiTracker.WifiTrackerNetworkCallback mNetworkCallback; 135 136 /** 137 * Synchronization lock for managing concurrency between main and worker threads. 138 * 139 * <p>This lock should be held for all modifications to {@link #mInternalAccessPoints} and 140 * {@link #mScanner}. 141 */ 142 private final Object mLock = new Object(); 143 144 /** The list of AccessPoints, aggregated visible ScanResults with metadata. */ 145 @GuardedBy("mLock") 146 private final List<AccessPoint> mInternalAccessPoints = new ArrayList<>(); 147 148 @GuardedBy("mLock") 149 private final Set<NetworkKey> mRequestedScores = new ArraySet<>(); 150 151 /** 152 * Tracks whether fresh scan results have been received since scanning start. 153 * 154 * <p>If this variable is false, we will not invoke callbacks so that we do not 155 * update the UI with stale data / clear out existing UI elements prematurely. 156 */ 157 private boolean mStaleScanResults = true; 158 159 /** 160 * Tracks whether the latest SCAN_RESULTS_AVAILABLE_ACTION contained new scans. If not, then 161 * we treat the last scan as an aborted scan and increase the eviction timeout window to avoid 162 * completely flushing the AP list before the next successful scan completes. 163 */ 164 private boolean mLastScanSucceeded = true; 165 166 // Does not need to be locked as it only updated on the worker thread, with the exception of 167 // during onStart, which occurs before the receiver is registered on the work handler. 168 private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>(); 169 private boolean mRegistered; 170 171 private NetworkInfo mLastNetworkInfo; 172 private WifiInfo mLastInfo; 173 174 private final NetworkScoreManager mNetworkScoreManager; 175 private WifiNetworkScoreCache mScoreCache; 176 private boolean mNetworkScoringUiEnabled; 177 private long mMaxSpeedLabelScoreCacheAge; 178 179 private static final String WIFI_SECURITY_PSK = "PSK"; 180 private static final String WIFI_SECURITY_EAP = "EAP"; 181 private static final String WIFI_SECURITY_SAE = "SAE"; 182 private static final String WIFI_SECURITY_OWE = "OWE"; 183 private static final String WIFI_SECURITY_SUITE_B_192 = "SUITE_B_192"; 184 185 @GuardedBy("mLock") 186 @VisibleForTesting 187 WifiTracker.Scanner mScanner; 188 newIntentFilter()189 private static IntentFilter newIntentFilter() { 190 IntentFilter filter = new IntentFilter(); 191 filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); 192 filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 193 filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION); 194 filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); 195 filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); 196 filter.addAction(WifiManager.ACTION_LINK_CONFIGURATION_CHANGED); 197 filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); 198 filter.addAction(WifiManager.RSSI_CHANGED_ACTION); 199 200 return filter; 201 } 202 203 /** 204 * Use the lifecycle constructor below whenever possible 205 */ 206 @Deprecated WifiTracker(Context context, WifiTracker.WifiListener wifiListener, boolean includeSaved, boolean includeScans)207 public WifiTracker(Context context, WifiTracker.WifiListener wifiListener, 208 boolean includeSaved, boolean includeScans) { 209 this(context, wifiListener, 210 context.getSystemService(WifiManager.class), 211 context.getSystemService(ConnectivityManager.class), 212 context.getSystemService(NetworkScoreManager.class), 213 newIntentFilter()); 214 } 215 216 // calling apps once IC window is complete WifiTracker(Context context, WifiTracker.WifiListener wifiListener, @NonNull Lifecycle lifecycle, boolean includeSaved, boolean includeScans)217 public WifiTracker(Context context, WifiTracker.WifiListener wifiListener, 218 @NonNull Lifecycle lifecycle, boolean includeSaved, boolean includeScans) { 219 this(context, wifiListener, 220 context.getSystemService(WifiManager.class), 221 context.getSystemService(ConnectivityManager.class), 222 context.getSystemService(NetworkScoreManager.class), 223 newIntentFilter()); 224 225 lifecycle.addObserver(this); 226 } 227 228 @VisibleForTesting WifiTracker(Context context, WifiTracker.WifiListener wifiListener, WifiManager wifiManager, ConnectivityManager connectivityManager, NetworkScoreManager networkScoreManager, IntentFilter filter)229 WifiTracker(Context context, WifiTracker.WifiListener wifiListener, 230 WifiManager wifiManager, ConnectivityManager connectivityManager, 231 NetworkScoreManager networkScoreManager, 232 IntentFilter filter) { 233 mContext = context; 234 mWifiManager = wifiManager; 235 mListener = new WifiTracker.WifiListenerExecutor(wifiListener); 236 mConnectivityManager = connectivityManager; 237 238 // check if verbose logging developer option has been turned on or off 239 sVerboseLogging = mWifiManager != null && mWifiManager.isVerboseLoggingEnabled(); 240 241 mFilter = filter; 242 243 mNetworkRequest = new NetworkRequest.Builder() 244 .clearCapabilities() 245 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 246 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 247 .build(); 248 249 mNetworkScoreManager = networkScoreManager; 250 251 final HandlerThread workThread = new HandlerThread(TAG 252 + "{" + Integer.toHexString(System.identityHashCode(this)) + "}", 253 Process.THREAD_PRIORITY_BACKGROUND); 254 workThread.start(); 255 setWorkThread(workThread); 256 } 257 258 /** 259 * Validity warning: this wipes out mScoreCache, so use with extreme caution 260 * 261 * @param workThread substitute Handler thread, for testing purposes only 262 */ 263 @VisibleForTesting 264 // during construction setWorkThread(HandlerThread workThread)265 void setWorkThread(HandlerThread workThread) { 266 mWorkThread = workThread; 267 mWorkHandler = new Handler(workThread.getLooper()); 268 mScoreCache = new WifiNetworkScoreCache(mContext, new CacheListener(mWorkHandler) { 269 @Override 270 public void networkCacheUpdated(List<ScoredNetwork> networks) { 271 if (!mRegistered) return; 272 273 if (Log.isLoggable(TAG, Log.VERBOSE)) { 274 Log.v(TAG, "Score cache was updated with networks: " + networks); 275 } 276 updateNetworkScores(); 277 } 278 }); 279 } 280 281 @Override onDestroy()282 public void onDestroy() { 283 mWorkThread.quit(); 284 } 285 286 /** 287 * Temporarily stop scanning for wifi networks. 288 * 289 * <p>Sets {@link #mStaleScanResults} to true. 290 */ pauseScanning()291 private void pauseScanning() { 292 synchronized (mLock) { 293 if (mScanner != null) { 294 mScanner.pause(); 295 mScanner = null; 296 } 297 } 298 mStaleScanResults = true; 299 } 300 301 /** 302 * Resume scanning for wifi networks after it has been paused. 303 * 304 * <p>The score cache should be registered before this method is invoked. 305 */ resumeScanning()306 public void resumeScanning() { 307 synchronized (mLock) { 308 if (mScanner == null) { 309 mScanner = new WifiTracker.Scanner(); 310 } 311 312 if (isWifiEnabled()) { 313 mScanner.resume(); 314 } 315 } 316 } 317 318 /** 319 * Start tracking wifi networks and scores. 320 * 321 * <p>Registers listeners and starts scanning for wifi networks. If this is not called 322 * then forceUpdate() must be called to populate getAccessPoints(). 323 */ 324 @Override 325 @MainThread onStart()326 public void onStart() { 327 // fetch current ScanResults instead of waiting for broadcast of fresh results 328 forceUpdate(); 329 330 registerScoreCache(); 331 332 mNetworkScoringUiEnabled = 333 Settings.Global.getInt( 334 mContext.getContentResolver(), 335 Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1; 336 337 mMaxSpeedLabelScoreCacheAge = 338 Settings.Global.getLong( 339 mContext.getContentResolver(), 340 Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS, 341 DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS); 342 343 resumeScanning(); 344 if (!mRegistered) { 345 mContext.registerReceiver(mReceiver, mFilter, null /* permission */, mWorkHandler, 346 Context.RECEIVER_EXPORTED_UNAUDITED); 347 // NetworkCallback objects cannot be reused. http://b/20701525 . 348 mNetworkCallback = new WifiTracker.WifiTrackerNetworkCallback(); 349 mConnectivityManager.registerNetworkCallback( 350 mNetworkRequest, mNetworkCallback, mWorkHandler); 351 mRegistered = true; 352 } 353 } 354 355 356 /** 357 * Synchronously update the list of access points with the latest information. 358 * 359 * <p>Intended to only be invoked within {@link #onStart()}. 360 */ 361 @MainThread 362 @VisibleForTesting forceUpdate()363 void forceUpdate() { 364 mLastInfo = mWifiManager.getConnectionInfo(); 365 mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork()); 366 367 fetchScansAndConfigsAndUpdateAccessPoints(); 368 } 369 registerScoreCache()370 private void registerScoreCache() { 371 mNetworkScoreManager.registerNetworkScoreCache( 372 NetworkKey.TYPE_WIFI, 373 mScoreCache, 374 NetworkScoreManager.SCORE_FILTER_SCAN_RESULTS); 375 } 376 requestScoresForNetworkKeys(Collection<NetworkKey> keys)377 private void requestScoresForNetworkKeys(Collection<NetworkKey> keys) { 378 if (keys.isEmpty()) return; 379 380 if (DBG()) { 381 Log.d(TAG, "Requesting scores for Network Keys: " + keys); 382 } 383 mNetworkScoreManager.requestScores(keys.toArray(new NetworkKey[keys.size()])); 384 synchronized (mLock) { 385 mRequestedScores.addAll(keys); 386 } 387 } 388 389 /** 390 * Stop tracking wifi networks and scores. 391 * 392 * <p>This should always be called when done with a WifiTracker (if onStart was called) to 393 * ensure proper cleanup and prevent any further callbacks from occurring. 394 * 395 * <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents 396 * {@link WifiTracker.WifiListener#onAccessPointsChanged()} callbacks from being invoked (until 397 * the bit 398 * is unset on the next SCAN_RESULTS_AVAILABLE_ACTION). 399 */ 400 @Override 401 @MainThread onStop()402 public void onStop() { 403 if (mRegistered) { 404 mContext.unregisterReceiver(mReceiver); 405 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 406 mRegistered = false; 407 } 408 unregisterScoreCache(); 409 pauseScanning(); // and set mStaleScanResults 410 411 mWorkHandler.removeCallbacksAndMessages(null /* remove all */); 412 } 413 unregisterScoreCache()414 private void unregisterScoreCache() { 415 mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mScoreCache); 416 417 // We do not want to clear the existing scores in the cache, as this method is called during 418 // stop tracking on activity pause. Hence, on resumption we want the ability to show the 419 // last known, potentially stale, scores. However, by clearing requested scores, the scores 420 // will be requested again upon resumption of tracking, and if any changes have occurred 421 // the listeners (UI) will be updated accordingly. 422 synchronized (mLock) { 423 mRequestedScores.clear(); 424 } 425 } 426 427 /** 428 * Gets the current list of access points. 429 * 430 * <p>This method is can be called on an abitrary thread by clients, but is normally called on 431 * the UI Thread by the rendering App. 432 */ 433 @AnyThread getAccessPoints()434 public List<AccessPoint> getAccessPoints() { 435 synchronized (mLock) { 436 return new ArrayList<>(mInternalAccessPoints); 437 } 438 } 439 getManager()440 public WifiManager getManager() { 441 return mWifiManager; 442 } 443 isWifiEnabled()444 public boolean isWifiEnabled() { 445 return mWifiManager != null && mWifiManager.isWifiEnabled(); 446 } 447 448 /** 449 * Returns the number of saved networks on the device, regardless of whether the WifiTracker 450 * is tracking saved networks. 451 * TODO(b/62292448): remove this function and update callsites to use WifiSavedConfigUtils 452 * directly. 453 */ getNumSavedNetworks()454 public int getNumSavedNetworks() { 455 return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size(); 456 } 457 isConnected()458 public boolean isConnected() { 459 return mConnected.get(); 460 } 461 dump(PrintWriter pw)462 public void dump(PrintWriter pw) { 463 pw.println(" - wifi tracker ------"); 464 for (AccessPoint accessPoint : getAccessPoints()) { 465 pw.println(" " + accessPoint); 466 } 467 } 468 updateScanResultCache( final List<ScanResult> newResults)469 private ArrayMap<String, List<ScanResult>> updateScanResultCache( 470 final List<ScanResult> newResults) { 471 // memory efficiency 472 for (ScanResult newResult : newResults) { 473 if (newResult.SSID == null || newResult.SSID.isEmpty()) { 474 continue; 475 } 476 mScanResultCache.put(newResult.BSSID, newResult); 477 } 478 479 // Evict old results in all conditions 480 evictOldScans(); 481 482 ArrayMap<String, List<ScanResult>> scanResultsByApKey = new ArrayMap<>(); 483 for (ScanResult result : mScanResultCache.values()) { 484 // Ignore hidden and ad-hoc networks. 485 if (result.SSID == null || result.SSID.length() == 0 || 486 result.capabilities.contains("[IBSS]")) { 487 continue; 488 } 489 490 String apKey = AccessPoint.getKey(mContext, result); 491 List<ScanResult> resultList; 492 if (scanResultsByApKey.containsKey(apKey)) { 493 resultList = scanResultsByApKey.get(apKey); 494 } else { 495 resultList = new ArrayList<>(); 496 scanResultsByApKey.put(apKey, resultList); 497 } 498 499 resultList.add(result); 500 } 501 502 return scanResultsByApKey; 503 } 504 505 /** 506 * Remove old scan results from the cache. If {@link #mLastScanSucceeded} is false, then 507 * increase the timeout window to avoid completely flushing the AP list before the next 508 * successful scan completes. 509 * 510 * <p>Should only ever be invoked from {@link #updateScanResultCache(List)} when 511 * {@link #mStaleScanResults} is false. 512 */ evictOldScans()513 private void evictOldScans() { 514 long evictionTimeoutMillis = mLastScanSucceeded ? MAX_SCAN_RESULT_AGE_MILLIS 515 : MAX_SCAN_RESULT_AGE_MILLIS * 2; 516 517 long nowMs = SystemClock.elapsedRealtime(); 518 for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) { 519 ScanResult result = iter.next(); 520 // result timestamp is in microseconds 521 if (nowMs - result.timestamp / 1000 > evictionTimeoutMillis) { 522 iter.remove(); 523 } 524 } 525 } 526 getWifiConfigurationForNetworkId( int networkId, final List<WifiConfiguration> configs)527 private WifiConfiguration getWifiConfigurationForNetworkId( 528 int networkId, final List<WifiConfiguration> configs) { 529 if (configs != null) { 530 for (WifiConfiguration config : configs) { 531 if (mLastInfo != null && networkId == config.networkId) { 532 return config; 533 } 534 } 535 } 536 return null; 537 } 538 539 /** 540 * Retrieves latest scan results and wifi configs, then calls 541 * {@link #updateAccessPoints(List, List)}. 542 */ fetchScansAndConfigsAndUpdateAccessPoints()543 private void fetchScansAndConfigsAndUpdateAccessPoints() { 544 List<ScanResult> newScanResults = mWifiManager.getScanResults(); 545 546 // Filter all unsupported networks from the scan result list 547 final List<ScanResult> filteredScanResults = 548 filterScanResultsByCapabilities(newScanResults); 549 550 if (isVerboseLoggingEnabled()) { 551 Log.i(TAG, "Fetched scan results: " + filteredScanResults); 552 } 553 554 List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks(); 555 updateAccessPoints(filteredScanResults, configs); 556 } 557 558 /** Update the internal list of access points. */ updateAccessPoints(final List<ScanResult> newScanResults, List<WifiConfiguration> configs)559 private void updateAccessPoints(final List<ScanResult> newScanResults, 560 List<WifiConfiguration> configs) { 561 562 WifiConfiguration connectionConfig = null; 563 if (mLastInfo != null) { 564 connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(), configs); 565 } 566 567 // Rather than dropping and reacquiring the lock multiple times in this method, we lock 568 // once for efficiency of lock acquisition time and readability 569 synchronized (mLock) { 570 ArrayMap<String, List<ScanResult>> scanResultsByApKey = 571 updateScanResultCache(newScanResults); 572 573 // Swap the current access points into a cached list for maintaining AP listeners 574 List<AccessPoint> cachedAccessPoints; 575 cachedAccessPoints = new ArrayList<>(mInternalAccessPoints); 576 577 ArrayList<AccessPoint> accessPoints = new ArrayList<>(); 578 579 final List<NetworkKey> scoresToRequest = new ArrayList<>(); 580 581 for (Map.Entry<String, List<ScanResult>> entry : scanResultsByApKey.entrySet()) { 582 for (ScanResult result : entry.getValue()) { 583 NetworkKey key = NetworkKey.createFromScanResult(result); 584 if (key != null && !mRequestedScores.contains(key)) { 585 scoresToRequest.add(key); 586 } 587 } 588 589 AccessPoint accessPoint = 590 getCachedOrCreate(entry.getValue(), cachedAccessPoints); 591 592 // Update the matching config if there is one, to populate saved network info 593 final List<WifiConfiguration> matchedConfigs = configs.stream() 594 .filter(config -> accessPoint.matches(config)) 595 .collect(Collectors.toList()); 596 597 final int matchedConfigCount = matchedConfigs.size(); 598 if (matchedConfigCount == 0) { 599 accessPoint.update(null); 600 } else if (matchedConfigCount == 1) { 601 accessPoint.update(matchedConfigs.get(0)); 602 } else { 603 // We may have 2 matched configured WifiCongiguration if the AccessPoint is 604 // of PSK/SAE transition mode or open/OWE transition mode. 605 Optional<WifiConfiguration> preferredConfig = matchedConfigs.stream() 606 .filter(config -> isSaeOrOwe(config)).findFirst(); 607 if (preferredConfig.isPresent()) { 608 accessPoint.update(preferredConfig.get()); 609 } else { 610 accessPoint.update(matchedConfigs.get(0)); 611 } 612 } 613 614 accessPoints.add(accessPoint); 615 } 616 617 List<ScanResult> cachedScanResults = new ArrayList<>(mScanResultCache.values()); 618 619 // Add a unique Passpoint AccessPoint for each Passpoint profile's unique identifier. 620 accessPoints.addAll(updatePasspointAccessPoints( 621 mWifiManager.getAllMatchingWifiConfigs(cachedScanResults), cachedAccessPoints)); 622 623 // Add OSU Provider AccessPoints 624 accessPoints.addAll(updateOsuAccessPoints( 625 mWifiManager.getMatchingOsuProviders(cachedScanResults), cachedAccessPoints)); 626 627 if (mLastInfo != null && mLastNetworkInfo != null) { 628 for (AccessPoint ap : accessPoints) { 629 ap.update(connectionConfig, mLastInfo, mLastNetworkInfo); 630 } 631 } 632 633 // If there were no scan results, create an AP for the currently connected network (if 634 // it exists). 635 if (accessPoints.isEmpty() && connectionConfig != null) { 636 AccessPoint 637 activeAp = new AccessPoint(mContext, connectionConfig); 638 activeAp.update(connectionConfig, mLastInfo, mLastNetworkInfo); 639 accessPoints.add(activeAp); 640 scoresToRequest.add(NetworkKey.createFromWifiInfo(mLastInfo)); 641 } 642 643 requestScoresForNetworkKeys(scoresToRequest); 644 for (AccessPoint ap : accessPoints) { 645 ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge); 646 } 647 648 // Pre-sort accessPoints to speed preference insertion 649 Collections.sort(accessPoints); 650 651 // Log accesspoints that are being removed 652 if (DBG()) { 653 Log.d(TAG, 654 "------ Dumping AccessPoints that were not seen on this scan ------"); 655 for (AccessPoint prevAccessPoint : mInternalAccessPoints) { 656 String prevTitle = prevAccessPoint.getTitle(); 657 boolean found = false; 658 for (AccessPoint newAccessPoint : accessPoints) { 659 if (newAccessPoint.getTitle() != null && newAccessPoint.getTitle() 660 .equals(prevTitle)) { 661 found = true; 662 break; 663 } 664 } 665 if (!found) { 666 Log.d(TAG, "Did not find " + prevTitle + " in this scan"); 667 } 668 } 669 Log.d(TAG, 670 "---- Done dumping AccessPoints that were not seen on this scan ----"); 671 } 672 673 mInternalAccessPoints.clear(); 674 mInternalAccessPoints.addAll(accessPoints); 675 } 676 677 conditionallyNotifyListeners(); 678 } 679 isSaeOrOwe(WifiConfiguration config)680 private static boolean isSaeOrOwe(WifiConfiguration config) { 681 final int security = AccessPoint.getSecurity(config); 682 return security == AccessPoint.SECURITY_SAE || security == AccessPoint.SECURITY_OWE; 683 } 684 685 @VisibleForTesting updatePasspointAccessPoints( List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> passpointConfigsAndScans, List<AccessPoint> accessPointCache)686 List<AccessPoint> updatePasspointAccessPoints( 687 List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> passpointConfigsAndScans, 688 List<AccessPoint> accessPointCache) { 689 List<AccessPoint> accessPoints = new ArrayList<>(); 690 691 Set<String> seenFQDNs = new ArraySet<>(); 692 for (Pair<WifiConfiguration, 693 Map<Integer, List<ScanResult>>> pairing : passpointConfigsAndScans) { 694 WifiConfiguration config = pairing.first; 695 if (seenFQDNs.add(config.FQDN)) { 696 List<ScanResult> homeScans = 697 pairing.second.get(WifiManager.PASSPOINT_HOME_NETWORK); 698 List<ScanResult> roamingScans = 699 pairing.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK); 700 701 AccessPoint accessPoint = 702 getCachedOrCreatePasspoint(config, homeScans, roamingScans, 703 accessPointCache); 704 accessPoints.add(accessPoint); 705 } 706 } 707 return accessPoints; 708 } 709 710 @VisibleForTesting updateOsuAccessPoints( Map<OsuProvider, List<ScanResult>> providersAndScans, List<AccessPoint> accessPointCache)711 List<AccessPoint> updateOsuAccessPoints( 712 Map<OsuProvider, List<ScanResult>> providersAndScans, 713 List<AccessPoint> accessPointCache) { 714 List<AccessPoint> accessPoints = new ArrayList<>(); 715 716 Set<OsuProvider> alreadyProvisioned = mWifiManager 717 .getMatchingPasspointConfigsForOsuProviders( 718 providersAndScans.keySet()).keySet(); 719 for (OsuProvider provider : providersAndScans.keySet()) { 720 if (!alreadyProvisioned.contains(provider)) { 721 AccessPoint accessPointOsu = 722 getCachedOrCreateOsu(provider, providersAndScans.get(provider), 723 accessPointCache); 724 accessPoints.add(accessPointOsu); 725 } 726 } 727 return accessPoints; 728 } 729 getCachedOrCreate( List<ScanResult> scanResults, List<AccessPoint> cache)730 private AccessPoint getCachedOrCreate( 731 List<ScanResult> scanResults, 732 List<AccessPoint> cache) { 733 AccessPoint accessPoint = getCachedByKey(cache, 734 AccessPoint.getKey(mContext, scanResults.get(0))); 735 if (accessPoint == null) { 736 accessPoint = new AccessPoint(mContext, scanResults); 737 } else { 738 accessPoint.setScanResults(scanResults); 739 } 740 return accessPoint; 741 } 742 getCachedOrCreatePasspoint( WifiConfiguration config, List<ScanResult> homeScans, List<ScanResult> roamingScans, List<AccessPoint> cache)743 private AccessPoint getCachedOrCreatePasspoint( 744 WifiConfiguration config, 745 List<ScanResult> homeScans, 746 List<ScanResult> roamingScans, 747 List<AccessPoint> cache) { 748 AccessPoint 749 accessPoint = getCachedByKey(cache, AccessPoint.getKey(config)); 750 if (accessPoint == null) { 751 accessPoint = new AccessPoint(mContext, config, homeScans, roamingScans); 752 } else { 753 accessPoint.update(config); 754 accessPoint.setScanResultsPasspoint(homeScans, roamingScans); 755 } 756 return accessPoint; 757 } 758 getCachedOrCreateOsu( OsuProvider provider, List<ScanResult> scanResults, List<AccessPoint> cache)759 private AccessPoint getCachedOrCreateOsu( 760 OsuProvider provider, 761 List<ScanResult> scanResults, 762 List<AccessPoint> cache) { 763 AccessPoint 764 accessPoint = getCachedByKey(cache, AccessPoint.getKey(provider)); 765 if (accessPoint == null) { 766 accessPoint = new AccessPoint(mContext, provider, scanResults); 767 } else { 768 accessPoint.setScanResults(scanResults); 769 } 770 return accessPoint; 771 } 772 getCachedByKey(List<AccessPoint> cache, String key)773 private AccessPoint getCachedByKey(List<AccessPoint> cache, String key) { 774 ListIterator<AccessPoint> lit = cache.listIterator(); 775 while (lit.hasNext()) { 776 AccessPoint currentAccessPoint = lit.next(); 777 if (currentAccessPoint.getKey().equals(key)) { 778 lit.remove(); 779 return currentAccessPoint; 780 } 781 } 782 return null; 783 } 784 updateNetworkInfo(NetworkInfo networkInfo)785 private void updateNetworkInfo(NetworkInfo networkInfo) { 786 /* Sticky broadcasts can call this when wifi is disabled */ 787 if (!isWifiEnabled()) { 788 clearAccessPointsAndConditionallyUpdate(); 789 return; 790 } 791 792 if (networkInfo != null) { 793 mLastNetworkInfo = networkInfo; 794 if (DBG()) { 795 Log.d(TAG, "mLastNetworkInfo set: " + mLastNetworkInfo); 796 } 797 798 if (networkInfo.isConnected() != mConnected.getAndSet(networkInfo.isConnected())) { 799 mListener.onConnectedChanged(); 800 } 801 } 802 803 WifiConfiguration connectionConfig = null; 804 805 mLastInfo = mWifiManager.getConnectionInfo(); 806 if (DBG()) { 807 Log.d(TAG, "mLastInfo set as: " + mLastInfo); 808 } 809 if (mLastInfo != null) { 810 connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(), 811 mWifiManager.getConfiguredNetworks()); 812 } 813 814 boolean updated = false; 815 boolean reorder = false; // Only reorder if connected AP was changed 816 817 synchronized (mLock) { 818 for (int i = mInternalAccessPoints.size() - 1; i >= 0; --i) { 819 AccessPoint ap = mInternalAccessPoints.get(i); 820 boolean previouslyConnected = ap.isActive(); 821 if (ap.update(connectionConfig, mLastInfo, mLastNetworkInfo)) { 822 updated = true; 823 if (previouslyConnected != ap.isActive()) reorder = true; 824 } 825 if (ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) { 826 reorder = true; 827 updated = true; 828 } 829 } 830 831 if (reorder) { 832 Collections.sort(mInternalAccessPoints); 833 } 834 if (updated) { 835 conditionallyNotifyListeners(); 836 } 837 } 838 } 839 840 /** 841 * Clears the access point list and conditionally invokes 842 * {@link WifiTracker.WifiListener#onAccessPointsChanged()} if required (i.e. the list was not 843 * already 844 * empty). 845 */ clearAccessPointsAndConditionallyUpdate()846 private void clearAccessPointsAndConditionallyUpdate() { 847 synchronized (mLock) { 848 if (!mInternalAccessPoints.isEmpty()) { 849 mInternalAccessPoints.clear(); 850 conditionallyNotifyListeners(); 851 } 852 } 853 } 854 855 /** 856 * Update all the internal access points rankingScores, badge and metering. 857 * 858 * <p>Will trigger a resort and notify listeners of changes if applicable. 859 * 860 * <p>Synchronized on {@link #mLock}. 861 */ updateNetworkScores()862 private void updateNetworkScores() { 863 synchronized (mLock) { 864 boolean updated = false; 865 for (int i = 0; i < mInternalAccessPoints.size(); i++) { 866 if (mInternalAccessPoints.get(i).update( 867 mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) { 868 updated = true; 869 } 870 } 871 if (updated) { 872 Collections.sort(mInternalAccessPoints); 873 conditionallyNotifyListeners(); 874 } 875 } 876 } 877 878 /** 879 * Receiver for handling broadcasts. 880 * 881 * This receiver is registered on the WorkHandler. 882 */ 883 @VisibleForTesting 884 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 885 @Override 886 public void onReceive(Context context, Intent intent) { 887 String action = intent.getAction(); 888 889 if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { 890 updateWifiState( 891 intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 892 WifiManager.WIFI_STATE_UNKNOWN)); 893 } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) { 894 mStaleScanResults = false; 895 mLastScanSucceeded = 896 intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); 897 898 fetchScansAndConfigsAndUpdateAccessPoints(); 899 } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) 900 || WifiManager.ACTION_LINK_CONFIGURATION_CHANGED.equals(action)) { 901 fetchScansAndConfigsAndUpdateAccessPoints(); 902 } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { 903 // onAccessPointsChanged updates being called from this intent. 904 NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); 905 updateNetworkInfo(info); 906 fetchScansAndConfigsAndUpdateAccessPoints(); 907 } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) { 908 updateNetworkInfo(/* networkInfo= */ null); 909 } 910 } 911 }; 912 913 /** 914 * Handles updates to WifiState. 915 * 916 * <p>If Wifi is not enabled in the enabled state, {@link #mStaleScanResults} will be set to 917 * true. 918 */ updateWifiState(int state)919 private void updateWifiState(int state) { 920 if (isVerboseLoggingEnabled()) { 921 Log.d(TAG, "updateWifiState: " + state); 922 } 923 if (state == WifiManager.WIFI_STATE_ENABLED) { 924 synchronized (mLock) { 925 if (mScanner != null) { 926 // We only need to resume if mScanner isn't null because 927 // that means we want to be scanning. 928 mScanner.resume(); 929 } 930 } 931 } else { 932 clearAccessPointsAndConditionallyUpdate(); 933 mLastInfo = null; 934 mLastNetworkInfo = null; 935 synchronized (mLock) { 936 if (mScanner != null) { 937 mScanner.pause(); 938 } 939 } 940 mStaleScanResults = true; 941 } 942 mListener.onWifiStateChanged(state); 943 } 944 945 private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback { onCapabilitiesChanged(Network network, NetworkCapabilities nc)946 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { 947 if (network.equals(mWifiManager.getCurrentNetwork())) { 948 // more sense fetch the latest network info here: 949 950 // We don't send a NetworkInfo object along with this message, because even if we 951 // fetch one from ConnectivityManager, it might be older than the most recent 952 // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast. 953 updateNetworkInfo(/* networkInfo= */ null); 954 } 955 } 956 } 957 958 @VisibleForTesting 959 class Scanner extends Handler { 960 static final int MSG_SCAN = 0; 961 962 private int mRetry = 0; 963 resume()964 void resume() { 965 if (isVerboseLoggingEnabled()) { 966 Log.d(TAG, "Scanner resume"); 967 } 968 if (!hasMessages(MSG_SCAN)) { 969 sendEmptyMessage(MSG_SCAN); 970 } 971 } 972 pause()973 void pause() { 974 if (isVerboseLoggingEnabled()) { 975 Log.d(TAG, "Scanner pause"); 976 } 977 mRetry = 0; 978 removeMessages(MSG_SCAN); 979 } 980 981 @VisibleForTesting isScanning()982 boolean isScanning() { 983 return hasMessages(MSG_SCAN); 984 } 985 986 @Override handleMessage(Message message)987 public void handleMessage(Message message) { 988 if (message.what != MSG_SCAN) return; 989 if (mWifiManager.startScan()) { 990 mRetry = 0; 991 } else if (++mRetry >= 3) { 992 mRetry = 0; 993 if (mContext != null) { 994 // Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG) 995 // .show(); 996 } 997 return; 998 } 999 sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS); 1000 } 1001 } 1002 1003 /** A restricted multimap for use in constructAccessPoints */ 1004 private static class Multimap<K, V> { 1005 private final HashMap<K, List<V>> store = new HashMap<K, List<V>>(); 1006 1007 /** retrieve a non-null list of values with key K */ getAll(K key)1008 List<V> getAll(K key) { 1009 List<V> values = store.get(key); 1010 return values != null ? values : Collections.emptyList(); 1011 } 1012 put(K key, V val)1013 void put(K key, V val) { 1014 List<V> curVals = store.get(key); 1015 if (curVals == null) { 1016 curVals = new ArrayList<V>(3); 1017 store.put(key, curVals); 1018 } 1019 curVals.add(val); 1020 } 1021 } 1022 1023 /** 1024 * Wraps the given {@link WifiTracker.WifiListener} instance and executes its methods on the 1025 * Main Thread. 1026 * 1027 * <p>Also logs all callbacks invocations when verbose logging is enabled. 1028 */ 1029 @VisibleForTesting 1030 class WifiListenerExecutor implements 1031 WifiTracker.WifiListener { 1032 1033 private final WifiTracker.WifiListener mDelegatee; 1034 WifiListenerExecutor(WifiTracker.WifiListener listener)1035 public WifiListenerExecutor(WifiTracker.WifiListener listener) { 1036 mDelegatee = listener; 1037 } 1038 1039 @Override onWifiStateChanged(int state)1040 public void onWifiStateChanged(int state) { 1041 runAndLog(() -> mDelegatee.onWifiStateChanged(state), 1042 String.format("Invoking onWifiStateChanged callback with state %d", state)); 1043 } 1044 1045 @Override onConnectedChanged()1046 public void onConnectedChanged() { 1047 runAndLog(mDelegatee::onConnectedChanged, "Invoking onConnectedChanged callback"); 1048 } 1049 1050 @Override onAccessPointsChanged()1051 public void onAccessPointsChanged() { 1052 runAndLog(mDelegatee::onAccessPointsChanged, "Invoking onAccessPointsChanged callback"); 1053 } 1054 runAndLog(Runnable r, String verboseLog)1055 private void runAndLog(Runnable r, String verboseLog) { 1056 ThreadUtils.postOnMainThread(() -> { 1057 if (mRegistered) { 1058 if (isVerboseLoggingEnabled()) { 1059 Log.i(TAG, verboseLog); 1060 } 1061 r.run(); 1062 } 1063 }); 1064 } 1065 } 1066 1067 /** 1068 * WifiListener interface that defines callbacks indicating state changes in WifiTracker. 1069 * 1070 * <p>All callbacks are invoked on the MainThread. 1071 */ 1072 public interface WifiListener { 1073 /** 1074 * Called when the state of Wifi has changed, the state will be one of 1075 * the following. 1076 * 1077 * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li> 1078 * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li> 1079 * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li> 1080 * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li> 1081 * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li> 1082 * <p> 1083 * 1084 * @param state The new state of wifi. 1085 */ onWifiStateChanged(int state)1086 void onWifiStateChanged(int state); 1087 1088 /** 1089 * Called when the connection state of wifi has changed and 1090 * {@link WifiTracker#isConnected()} should be called to get the updated state. 1091 */ onConnectedChanged()1092 void onConnectedChanged(); 1093 1094 /** 1095 * Called to indicate the list of AccessPoints has been updated and 1096 * {@link WifiTracker#getAccessPoints()} should be called to get the updated list. 1097 */ onAccessPointsChanged()1098 void onAccessPointsChanged(); 1099 } 1100 1101 /** 1102 * Invokes {@link WifiTracker.WifiListenerExecutor#onAccessPointsChanged()} iif {@link 1103 * #mStaleScanResults} 1104 * is false. 1105 */ conditionallyNotifyListeners()1106 private void conditionallyNotifyListeners() { 1107 if (mStaleScanResults) { 1108 return; 1109 } 1110 1111 mListener.onAccessPointsChanged(); 1112 } 1113 1114 /** 1115 * Filters unsupported networks from scan results. New WPA3 networks and OWE networks 1116 * may not be compatible with the device HW/SW. 1117 * 1118 * @param scanResults List of scan results 1119 * @return List of filtered scan results based on local device capabilities 1120 */ filterScanResultsByCapabilities(List<ScanResult> scanResults)1121 private List<ScanResult> filterScanResultsByCapabilities(List<ScanResult> scanResults) { 1122 if (scanResults == null) { 1123 return null; 1124 } 1125 1126 // Get and cache advanced capabilities 1127 final boolean isOweSupported = mWifiManager.isEnhancedOpenSupported(); 1128 final boolean isSaeSupported = mWifiManager.isWpa3SaeSupported(); 1129 final boolean isSuiteBSupported = mWifiManager.isWpa3SuiteBSupported(); 1130 1131 List<ScanResult> filteredScanResultList = new ArrayList<>(); 1132 1133 // Iterate through the list of scan results and filter out APs which are not 1134 // compatible with our device. 1135 for (ScanResult scanResult : scanResults) { 1136 if (scanResult.capabilities.contains(WIFI_SECURITY_PSK)) { 1137 // All devices (today) support RSN-PSK or WPA-PSK 1138 // Add this here because some APs may support both PSK and SAE and the check 1139 // below will filter it out. 1140 filteredScanResultList.add(scanResult); 1141 continue; 1142 } 1143 1144 if ((scanResult.capabilities.contains(WIFI_SECURITY_SUITE_B_192) && !isSuiteBSupported) 1145 || (scanResult.capabilities.contains(WIFI_SECURITY_SAE) && !isSaeSupported) 1146 || (scanResult.capabilities.contains(WIFI_SECURITY_OWE) && !isOweSupported)) { 1147 if (isVerboseLoggingEnabled()) { 1148 Log.v(TAG, "filterScanResultsByCapabilities: Filtering SSID " 1149 + scanResult.SSID + " with capabilities: " + scanResult.capabilities); 1150 } 1151 } else { 1152 // Safe to add 1153 filteredScanResultList.add(scanResult); 1154 } 1155 } 1156 1157 return filteredScanResultList; 1158 } 1159 } 1160