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