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