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.MainThread; 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.net.ConnectivityManager; 24 import android.net.Network; 25 import android.net.NetworkCapabilities; 26 import android.net.NetworkInfo; 27 import android.net.NetworkInfo.DetailedState; 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.Looper; 40 import android.os.Message; 41 import android.provider.Settings; 42 import android.support.annotation.GuardedBy; 43 import android.text.format.DateUtils; 44 import android.util.ArraySet; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.util.SparseIntArray; 48 import android.widget.Toast; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.settingslib.R; 52 53 import java.io.PrintWriter; 54 import java.util.ArrayList; 55 import java.util.Collection; 56 import java.util.Collections; 57 import java.util.HashMap; 58 import java.util.Iterator; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.Set; 62 import java.util.concurrent.atomic.AtomicBoolean; 63 64 /** 65 * Tracks saved or available wifi networks and their state. 66 */ 67 public class WifiTracker { 68 /** 69 * Default maximum age in millis of cached scored networks in 70 * {@link AccessPoint#mScoredNetworkCache} to be used for speed label generation. 71 */ 72 private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS; 73 74 private static final String TAG = "WifiTracker"; DBG()75 private static final boolean DBG() { 76 return Log.isLoggable(TAG, Log.DEBUG); 77 } 78 79 /** verbose logging flag. this flag is set thru developer debugging options 80 * and used so as to assist with in-the-field WiFi connectivity debugging */ 81 public static boolean sVerboseLogging; 82 83 // TODO(b/36733768): Remove flag includeSaved and includePasspoints. 84 85 // TODO: Allow control of this? 86 // Combo scans can take 5-6s to complete - set to 10s. 87 private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; 88 private static final int NUM_SCANS_TO_CONFIRM_AP_LOSS = 3; 89 90 private final Context mContext; 91 private final WifiManager mWifiManager; 92 private final IntentFilter mFilter; 93 private final ConnectivityManager mConnectivityManager; 94 private final NetworkRequest mNetworkRequest; 95 private final AtomicBoolean mConnected = new AtomicBoolean(false); 96 private final WifiListener mListener; 97 private final boolean mIncludeSaved; 98 private final boolean mIncludeScans; 99 private final boolean mIncludePasspoints; 100 @VisibleForTesting final MainHandler mMainHandler; 101 @VisibleForTesting final WorkHandler mWorkHandler; 102 103 private WifiTrackerNetworkCallback mNetworkCallback; 104 105 @GuardedBy("mLock") 106 private boolean mRegistered; 107 108 /** 109 * The externally visible access point list. 110 * 111 * Updated using main handler. Clone of this collection is returned from 112 * {@link #getAccessPoints()} 113 */ 114 private final List<AccessPoint> mAccessPoints = new ArrayList<>(); 115 116 /** 117 * The internal list of access points, synchronized on itself. 118 * 119 * Never exposed outside this class. 120 */ 121 @GuardedBy("mLock") 122 private final List<AccessPoint> mInternalAccessPoints = new ArrayList<>(); 123 124 /** 125 * Synchronization lock for managing concurrency between main and worker threads. 126 * 127 * <p>This lock should be held for all background work. 128 * TODO(b/37674366): Remove the worker thread so synchronization is no longer necessary. 129 */ 130 private final Object mLock = new Object(); 131 132 //visible to both worker and main thread. 133 @GuardedBy("mLock") 134 private final AccessPointListenerAdapter mAccessPointListenerAdapter 135 = new AccessPointListenerAdapter(); 136 137 private final HashMap<String, Integer> mSeenBssids = new HashMap<>(); 138 private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>(); 139 private Integer mScanId = 0; 140 141 private NetworkInfo mLastNetworkInfo; 142 private WifiInfo mLastInfo; 143 144 private final NetworkScoreManager mNetworkScoreManager; 145 private final WifiNetworkScoreCache mScoreCache; 146 private boolean mNetworkScoringUiEnabled; 147 private long mMaxSpeedLabelScoreCacheAge; 148 149 @GuardedBy("mLock") 150 private final Set<NetworkKey> mRequestedScores = new ArraySet<>(); 151 152 @VisibleForTesting 153 Scanner mScanner; 154 155 @GuardedBy("mLock") 156 private boolean mStaleScanResults = true; 157 WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans)158 public WifiTracker(Context context, WifiListener wifiListener, 159 boolean includeSaved, boolean includeScans) { 160 this(context, wifiListener, null, includeSaved, includeScans); 161 } 162 WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans)163 public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, 164 boolean includeSaved, boolean includeScans) { 165 this(context, wifiListener, workerLooper, includeSaved, includeScans, false); 166 } 167 WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans, boolean includePasspoints)168 public WifiTracker(Context context, WifiListener wifiListener, 169 boolean includeSaved, boolean includeScans, boolean includePasspoints) { 170 this(context, wifiListener, null, includeSaved, includeScans, includePasspoints); 171 } 172 WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints)173 public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, 174 boolean includeSaved, boolean includeScans, boolean includePasspoints) { 175 this(context, wifiListener, workerLooper, includeSaved, includeScans, includePasspoints, 176 context.getSystemService(WifiManager.class), 177 context.getSystemService(ConnectivityManager.class), 178 context.getSystemService(NetworkScoreManager.class), Looper.myLooper() 179 ); 180 } 181 182 @VisibleForTesting WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints, WifiManager wifiManager, ConnectivityManager connectivityManager, NetworkScoreManager networkScoreManager, Looper currentLooper)183 WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, 184 boolean includeSaved, boolean includeScans, boolean includePasspoints, 185 WifiManager wifiManager, ConnectivityManager connectivityManager, 186 NetworkScoreManager networkScoreManager, Looper currentLooper) { 187 if (!includeSaved && !includeScans) { 188 throw new IllegalArgumentException("Must include either saved or scans"); 189 } 190 mContext = context; 191 if (currentLooper == null) { 192 // When we aren't on a looper thread, default to the main. 193 currentLooper = Looper.getMainLooper(); 194 } 195 mMainHandler = new MainHandler(currentLooper); 196 mWorkHandler = new WorkHandler( 197 workerLooper != null ? workerLooper : currentLooper); 198 mWifiManager = wifiManager; 199 mIncludeSaved = includeSaved; 200 mIncludeScans = includeScans; 201 mIncludePasspoints = includePasspoints; 202 mListener = wifiListener; 203 mConnectivityManager = connectivityManager; 204 205 // check if verbose logging has been turned on or off 206 sVerboseLogging = (mWifiManager.getVerboseLoggingLevel() > 0); 207 208 mFilter = new IntentFilter(); 209 mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); 210 mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 211 mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION); 212 mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); 213 mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); 214 mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION); 215 mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); 216 mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); 217 218 mNetworkRequest = new NetworkRequest.Builder() 219 .clearCapabilities() 220 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 221 .build(); 222 223 mNetworkScoreManager = networkScoreManager; 224 225 mScoreCache = new WifiNetworkScoreCache(context, new CacheListener(mWorkHandler) { 226 @Override 227 public void networkCacheUpdated(List<ScoredNetwork> networks) { 228 synchronized (mLock) { 229 if (!mRegistered) return; 230 } 231 232 if (Log.isLoggable(TAG, Log.VERBOSE)) { 233 Log.v(TAG, "Score cache was updated with networks: " + networks); 234 } 235 updateNetworkScores(); 236 } 237 }); 238 } 239 240 /** Synchronously update the list of access points with the latest information. */ 241 @MainThread forceUpdate()242 public void forceUpdate() { 243 synchronized (mLock) { 244 mWorkHandler.removeMessages(WorkHandler.MSG_UPDATE_ACCESS_POINTS); 245 mLastInfo = mWifiManager.getConnectionInfo(); 246 mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork()); 247 248 final List<ScanResult> newScanResults = mWifiManager.getScanResults(); 249 if (sVerboseLogging) { 250 Log.i(TAG, "Fetched scan results: " + newScanResults); 251 } 252 253 List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks(); 254 mInternalAccessPoints.clear(); 255 updateAccessPointsLocked(newScanResults, configs); 256 257 // Synchronously copy access points 258 mMainHandler.removeMessages(MainHandler.MSG_ACCESS_POINT_CHANGED); 259 mMainHandler.handleMessage( 260 Message.obtain(mMainHandler, MainHandler.MSG_ACCESS_POINT_CHANGED)); 261 if (sVerboseLogging) { 262 Log.i(TAG, "force update - external access point list:\n" + mAccessPoints); 263 } 264 } 265 } 266 267 /** 268 * Force a scan for wifi networks to happen now. 269 */ forceScan()270 public void forceScan() { 271 if (mWifiManager.isWifiEnabled() && mScanner != null) { 272 mScanner.forceScan(); 273 } 274 } 275 276 /** 277 * Temporarily stop scanning for wifi networks. 278 */ pauseScanning()279 public void pauseScanning() { 280 if (mScanner != null) { 281 mScanner.pause(); 282 mScanner = null; 283 } 284 } 285 286 /** 287 * Resume scanning for wifi networks after it has been paused. 288 * 289 * <p>The score cache should be registered before this method is invoked. 290 */ resumeScanning()291 public void resumeScanning() { 292 if (mScanner == null) { 293 mScanner = new Scanner(); 294 } 295 296 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_RESUME); 297 if (mWifiManager.isWifiEnabled()) { 298 mScanner.resume(); 299 } 300 } 301 302 /** 303 * Start tracking wifi networks and scores. 304 * 305 * <p>Registers listeners and starts scanning for wifi networks. If this is not called 306 * then forceUpdate() must be called to populate getAccessPoints(). 307 */ 308 @MainThread startTracking()309 public void startTracking() { 310 synchronized (mLock) { 311 registerScoreCache(); 312 313 mNetworkScoringUiEnabled = 314 Settings.Global.getInt( 315 mContext.getContentResolver(), 316 Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1; 317 318 mMaxSpeedLabelScoreCacheAge = 319 Settings.Global.getLong( 320 mContext.getContentResolver(), 321 Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS, 322 DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS); 323 324 resumeScanning(); 325 if (!mRegistered) { 326 mContext.registerReceiver(mReceiver, mFilter); 327 // NetworkCallback objects cannot be reused. http://b/20701525 . 328 mNetworkCallback = new WifiTrackerNetworkCallback(); 329 mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback); 330 mRegistered = true; 331 } 332 } 333 } 334 registerScoreCache()335 private void registerScoreCache() { 336 mNetworkScoreManager.registerNetworkScoreCache( 337 NetworkKey.TYPE_WIFI, 338 mScoreCache, 339 NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS); 340 } 341 requestScoresForNetworkKeys(Collection<NetworkKey> keys)342 private void requestScoresForNetworkKeys(Collection<NetworkKey> keys) { 343 if (keys.isEmpty()) return; 344 345 if (DBG()) { 346 Log.d(TAG, "Requesting scores for Network Keys: " + keys); 347 } 348 mNetworkScoreManager.requestScores(keys.toArray(new NetworkKey[keys.size()])); 349 synchronized (mLock) { 350 mRequestedScores.addAll(keys); 351 } 352 } 353 354 /** 355 * Stop tracking wifi networks and scores. 356 * 357 * <p>This should always be called when done with a WifiTracker (if startTracking was called) to 358 * ensure proper cleanup and prevent any further callbacks from occurring. 359 * 360 * <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents 361 * {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit 362 * is unset on the next SCAN_RESULTS_AVAILABLE_ACTION). 363 */ 364 @MainThread stopTracking()365 public void stopTracking() { 366 synchronized (mLock) { 367 if (mRegistered) { 368 mContext.unregisterReceiver(mReceiver); 369 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 370 mRegistered = false; 371 } 372 unregisterScoreCache(); 373 pauseScanning(); 374 375 mWorkHandler.removePendingMessages(); 376 mMainHandler.removePendingMessages(); 377 mStaleScanResults = true; 378 } 379 } 380 unregisterScoreCache()381 private void unregisterScoreCache() { 382 mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mScoreCache); 383 384 // We do not want to clear the existing scores in the cache, as this method is called during 385 // stop tracking on activity pause. Hence, on resumption we want the ability to show the 386 // last known, potentially stale, scores. However, by clearing requested scores, the scores 387 // will be requested again upon resumption of tracking, and if any changes have occurred 388 // the listeners (UI) will be updated accordingly. 389 synchronized (mLock) { 390 mRequestedScores.clear(); 391 } 392 } 393 394 /** 395 * Gets the current list of access points. Should be called from main thread, otherwise 396 * expect inconsistencies 397 */ 398 @MainThread getAccessPoints()399 public List<AccessPoint> getAccessPoints() { 400 return new ArrayList<>(mAccessPoints); 401 } 402 getManager()403 public WifiManager getManager() { 404 return mWifiManager; 405 } 406 isWifiEnabled()407 public boolean isWifiEnabled() { 408 return mWifiManager.isWifiEnabled(); 409 } 410 411 /** 412 * Returns the number of saved networks on the device, regardless of whether the WifiTracker 413 * is tracking saved networks. 414 * TODO(b/62292448): remove this function and update callsites to use WifiSavedConfigUtils 415 * directly. 416 */ getNumSavedNetworks()417 public int getNumSavedNetworks() { 418 return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size(); 419 } 420 isConnected()421 public boolean isConnected() { 422 return mConnected.get(); 423 } 424 dump(PrintWriter pw)425 public void dump(PrintWriter pw) { 426 pw.println(" - wifi tracker ------"); 427 for (AccessPoint accessPoint : getAccessPoints()) { 428 pw.println(" " + accessPoint); 429 } 430 } 431 handleResume()432 private void handleResume() { 433 mScanResultCache.clear(); 434 mSeenBssids.clear(); 435 mScanId = 0; 436 } 437 updateScanResultCache(final List<ScanResult> newResults)438 private Collection<ScanResult> updateScanResultCache(final List<ScanResult> newResults) { 439 mScanId++; 440 for (ScanResult newResult : newResults) { 441 if (newResult.SSID == null || newResult.SSID.isEmpty()) { 442 continue; 443 } 444 mScanResultCache.put(newResult.BSSID, newResult); 445 mSeenBssids.put(newResult.BSSID, mScanId); 446 } 447 448 if (mScanId > NUM_SCANS_TO_CONFIRM_AP_LOSS) { 449 if (DBG()) Log.d(TAG, "------ Dumping SSIDs that were expired on this scan ------"); 450 Integer threshold = mScanId - NUM_SCANS_TO_CONFIRM_AP_LOSS; 451 for (Iterator<Map.Entry<String, Integer>> it = mSeenBssids.entrySet().iterator(); 452 it.hasNext(); /* nothing */) { 453 Map.Entry<String, Integer> e = it.next(); 454 if (e.getValue() < threshold) { 455 ScanResult result = mScanResultCache.get(e.getKey()); 456 if (DBG()) Log.d(TAG, "Removing " + e.getKey() + ":(" + result.SSID + ")"); 457 mScanResultCache.remove(e.getKey()); 458 it.remove(); 459 } 460 } 461 if (DBG()) Log.d(TAG, "---- Done Dumping SSIDs that were expired on this scan ----"); 462 } 463 464 return mScanResultCache.values(); 465 } 466 getWifiConfigurationForNetworkId( int networkId, final List<WifiConfiguration> configs)467 private WifiConfiguration getWifiConfigurationForNetworkId( 468 int networkId, final List<WifiConfiguration> configs) { 469 if (configs != null) { 470 for (WifiConfiguration config : configs) { 471 if (mLastInfo != null && networkId == config.networkId && 472 !(config.selfAdded && config.numAssociation == 0)) { 473 return config; 474 } 475 } 476 } 477 return null; 478 } 479 480 /** 481 * Safely modify {@link #mInternalAccessPoints} by acquiring {@link #mLock} first. 482 * 483 * <p>Will not perform the update if {@link #mStaleScanResults} is true 484 */ updateAccessPoints()485 private void updateAccessPoints() { 486 List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks(); 487 final List<ScanResult> newScanResults = mWifiManager.getScanResults(); 488 if (sVerboseLogging) { 489 Log.i(TAG, "Fetched scan results: " + newScanResults); 490 } 491 492 synchronized (mLock) { 493 if(!mStaleScanResults) { 494 updateAccessPointsLocked(newScanResults, configs); 495 } 496 } 497 } 498 499 /** 500 * Update the internal list of access points. 501 * 502 * <p>Do not called directly (except for forceUpdate), use {@link #updateAccessPoints()} which 503 * respects {@link #mStaleScanResults}. 504 */ 505 @GuardedBy("mLock") updateAccessPointsLocked(final List<ScanResult> newScanResults, List<WifiConfiguration> configs)506 private void updateAccessPointsLocked(final List<ScanResult> newScanResults, 507 List<WifiConfiguration> configs) { 508 WifiConfiguration connectionConfig = null; 509 if (mLastInfo != null) { 510 connectionConfig = getWifiConfigurationForNetworkId( 511 mLastInfo.getNetworkId(), mWifiManager.getConfiguredNetworks()); 512 } 513 514 // Swap the current access points into a cached list. 515 List<AccessPoint> cachedAccessPoints = new ArrayList<>(mInternalAccessPoints); 516 ArrayList<AccessPoint> accessPoints = new ArrayList<>(); 517 518 // Clear out the configs so we don't think something is saved when it isn't. 519 for (AccessPoint accessPoint : cachedAccessPoints) { 520 accessPoint.clearConfig(); 521 } 522 523 /* Lookup table to more quickly update AccessPoints by only considering objects with the 524 * correct SSID. Maps SSID -> List of AccessPoints with the given SSID. */ 525 Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>(); 526 527 final Collection<ScanResult> results = updateScanResultCache(newScanResults); 528 529 if (configs != null) { 530 for (WifiConfiguration config : configs) { 531 if (config.selfAdded && config.numAssociation == 0) { 532 continue; 533 } 534 AccessPoint accessPoint = getCachedOrCreate(config, cachedAccessPoints); 535 if (mLastInfo != null && mLastNetworkInfo != null) { 536 accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo); 537 } 538 if (mIncludeSaved) { 539 // If saved network not present in scan result then set its Rssi to 540 // UNREACHABLE_RSSI 541 boolean apFound = false; 542 for (ScanResult result : results) { 543 if (result.SSID.equals(accessPoint.getSsidStr())) { 544 apFound = true; 545 break; 546 } 547 } 548 if (!apFound) { 549 accessPoint.setUnreachable(); 550 } 551 accessPoints.add(accessPoint); 552 apMap.put(accessPoint.getSsidStr(), accessPoint); 553 } else { 554 // If we aren't using saved networks, drop them into the cache so that 555 // we have access to their saved info. 556 cachedAccessPoints.add(accessPoint); 557 } 558 } 559 } 560 561 final List<NetworkKey> scoresToRequest = new ArrayList<>(); 562 if (results != null) { 563 for (ScanResult result : results) { 564 // Ignore hidden and ad-hoc networks. 565 if (result.SSID == null || result.SSID.length() == 0 || 566 result.capabilities.contains("[IBSS]")) { 567 continue; 568 } 569 570 NetworkKey key = NetworkKey.createFromScanResult(result); 571 if (key != null && !mRequestedScores.contains(key)) { 572 scoresToRequest.add(key); 573 } 574 575 boolean found = false; 576 for (AccessPoint accessPoint : apMap.getAll(result.SSID)) { 577 // We want to evict old scan results if are current results are not stale 578 if (accessPoint.update(result, !mStaleScanResults)) { 579 found = true; 580 break; 581 } 582 } 583 if (!found && mIncludeScans) { 584 AccessPoint accessPoint = getCachedOrCreate(result, cachedAccessPoints); 585 if (mLastInfo != null && mLastNetworkInfo != null) { 586 accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo); 587 } 588 589 if (result.isPasspointNetwork()) { 590 // Retrieve a WifiConfiguration for a Passpoint provider that matches 591 // the given ScanResult. This is used for showing that a given AP 592 // (ScanResult) is available via a Passpoint provider (provider friendly 593 // name). 594 try { 595 WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result); 596 if (config != null) { 597 accessPoint.update(config); 598 } 599 } catch (UnsupportedOperationException e) { 600 // Passpoint not supported on the device. 601 } 602 } 603 604 accessPoints.add(accessPoint); 605 apMap.put(accessPoint.getSsidStr(), accessPoint); 606 } 607 } 608 } 609 610 requestScoresForNetworkKeys(scoresToRequest); 611 for (AccessPoint ap : accessPoints) { 612 ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge); 613 } 614 615 // Pre-sort accessPoints to speed preference insertion 616 Collections.sort(accessPoints); 617 618 // Log accesspoints that were deleted 619 if (DBG()) { 620 Log.d(TAG, "------ Dumping SSIDs that were not seen on this scan ------"); 621 for (AccessPoint prevAccessPoint : mInternalAccessPoints) { 622 if (prevAccessPoint.getSsid() == null) 623 continue; 624 String prevSsid = prevAccessPoint.getSsidStr(); 625 boolean found = false; 626 for (AccessPoint newAccessPoint : accessPoints) { 627 if (newAccessPoint.getSsidStr() != null && newAccessPoint.getSsidStr() 628 .equals(prevSsid)) { 629 found = true; 630 break; 631 } 632 } 633 if (!found) 634 Log.d(TAG, "Did not find " + prevSsid + " in this scan"); 635 } 636 Log.d(TAG, "---- Done dumping SSIDs that were not seen on this scan ----"); 637 } 638 639 mInternalAccessPoints.clear(); 640 mInternalAccessPoints.addAll(accessPoints); 641 642 mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED); 643 } 644 645 @VisibleForTesting getCachedOrCreate(ScanResult result, List<AccessPoint> cache)646 AccessPoint getCachedOrCreate(ScanResult result, List<AccessPoint> cache) { 647 final int N = cache.size(); 648 for (int i = 0; i < N; i++) { 649 if (cache.get(i).matches(result)) { 650 AccessPoint ret = cache.remove(i); 651 // evict old scan results only if we have fresh results 652 ret.update(result, !mStaleScanResults); 653 return ret; 654 } 655 } 656 final AccessPoint accessPoint = new AccessPoint(mContext, result); 657 accessPoint.setListener(mAccessPointListenerAdapter); 658 return accessPoint; 659 } 660 661 @VisibleForTesting getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache)662 AccessPoint getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache) { 663 final int N = cache.size(); 664 for (int i = 0; i < N; i++) { 665 if (cache.get(i).matches(config)) { 666 AccessPoint ret = cache.remove(i); 667 ret.loadConfig(config); 668 return ret; 669 } 670 } 671 final AccessPoint accessPoint = new AccessPoint(mContext, config); 672 accessPoint.setListener(mAccessPointListenerAdapter); 673 return accessPoint; 674 } 675 updateNetworkInfo(NetworkInfo networkInfo)676 private void updateNetworkInfo(NetworkInfo networkInfo) { 677 /* sticky broadcasts can call this when wifi is disabled */ 678 if (!mWifiManager.isWifiEnabled()) { 679 clearAccessPointsAndConditionallyUpdate(); 680 return; 681 } 682 683 if (networkInfo != null) { 684 mLastNetworkInfo = networkInfo; 685 if (DBG()) { 686 Log.d(TAG, "mLastNetworkInfo set: " + mLastNetworkInfo); 687 } 688 } 689 690 WifiConfiguration connectionConfig = null; 691 692 mLastInfo = mWifiManager.getConnectionInfo(); 693 if (DBG()) { 694 Log.d(TAG, "mLastInfo set as: " + mLastInfo); 695 } 696 if (mLastInfo != null) { 697 connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(), 698 mWifiManager.getConfiguredNetworks()); 699 } 700 701 boolean updated = false; 702 boolean reorder = false; // Only reorder if connected AP was changed 703 704 synchronized (mLock) { 705 for (int i = mInternalAccessPoints.size() - 1; i >= 0; --i) { 706 AccessPoint ap = mInternalAccessPoints.get(i); 707 boolean previouslyConnected = ap.isActive(); 708 if (ap.update(connectionConfig, mLastInfo, mLastNetworkInfo)) { 709 updated = true; 710 if (previouslyConnected != ap.isActive()) reorder = true; 711 } 712 if (ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) { 713 reorder = true; 714 updated = true; 715 } 716 } 717 718 if (reorder) Collections.sort(mInternalAccessPoints); 719 if (updated) mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED); 720 } 721 } 722 clearAccessPointsAndConditionallyUpdate()723 private void clearAccessPointsAndConditionallyUpdate() { 724 synchronized (mLock) { 725 if (!mInternalAccessPoints.isEmpty()) { 726 mInternalAccessPoints.clear(); 727 if (!mMainHandler.hasMessages(MainHandler.MSG_ACCESS_POINT_CHANGED)) { 728 mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED); 729 } 730 } 731 } 732 } 733 734 /** 735 * Update all the internal access points rankingScores, badge and metering. 736 * 737 * <p>Will trigger a resort and notify listeners of changes if applicable. 738 * 739 * <p>Synchronized on {@link #mLock}. 740 */ updateNetworkScores()741 private void updateNetworkScores() { 742 synchronized (mLock) { 743 boolean updated = false; 744 for (int i = 0; i < mInternalAccessPoints.size(); i++) { 745 if (mInternalAccessPoints.get(i).update( 746 mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) { 747 updated = true; 748 } 749 } 750 if (updated) { 751 Collections.sort(mInternalAccessPoints); 752 mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED); 753 } 754 } 755 } 756 updateWifiState(int state)757 private void updateWifiState(int state) { 758 mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_WIFI_STATE, state, 0).sendToTarget(); 759 if (!mWifiManager.isWifiEnabled()) { 760 clearAccessPointsAndConditionallyUpdate(); 761 } 762 } 763 getCurrentAccessPoints(Context context, boolean includeSaved, boolean includeScans, boolean includePasspoints)764 public static List<AccessPoint> getCurrentAccessPoints(Context context, boolean includeSaved, 765 boolean includeScans, boolean includePasspoints) { 766 WifiTracker tracker = new WifiTracker(context, 767 null, null, includeSaved, includeScans, includePasspoints); 768 tracker.forceUpdate(); 769 tracker.copyAndNotifyListeners(false /*notifyListeners*/); 770 return tracker.getAccessPoints(); 771 } 772 773 @VisibleForTesting 774 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 775 @Override 776 public void onReceive(Context context, Intent intent) { 777 String action = intent.getAction(); 778 779 if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { 780 updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 781 WifiManager.WIFI_STATE_UNKNOWN)); 782 } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) { 783 mWorkHandler 784 .obtainMessage( 785 WorkHandler.MSG_UPDATE_ACCESS_POINTS, 786 WorkHandler.CLEAR_STALE_SCAN_RESULTS, 787 0) 788 .sendToTarget(); 789 } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) 790 || WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) { 791 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS); 792 } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { 793 NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); 794 795 if(mConnected.get() != info.isConnected()) { 796 mConnected.set(info.isConnected()); 797 mMainHandler.sendEmptyMessage(MainHandler.MSG_CONNECTED_CHANGED); 798 } 799 800 mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO, info) 801 .sendToTarget(); 802 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS); 803 } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) { 804 NetworkInfo info = 805 mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork()); 806 mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO, info) 807 .sendToTarget(); 808 } 809 } 810 }; 811 812 private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback { onCapabilitiesChanged(Network network, NetworkCapabilities nc)813 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { 814 if (network.equals(mWifiManager.getCurrentNetwork())) { 815 // We don't send a NetworkInfo object along with this message, because even if we 816 // fetch one from ConnectivityManager, it might be older than the most recent 817 // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast. 818 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO); 819 } 820 } 821 } 822 823 @VisibleForTesting 824 final class MainHandler extends Handler { 825 @VisibleForTesting static final int MSG_CONNECTED_CHANGED = 0; 826 @VisibleForTesting static final int MSG_WIFI_STATE_CHANGED = 1; 827 @VisibleForTesting static final int MSG_ACCESS_POINT_CHANGED = 2; 828 private static final int MSG_RESUME_SCANNING = 3; 829 private static final int MSG_PAUSE_SCANNING = 4; 830 MainHandler(Looper looper)831 public MainHandler(Looper looper) { 832 super(looper); 833 } 834 835 @Override handleMessage(Message msg)836 public void handleMessage(Message msg) { 837 if (mListener == null) { 838 return; 839 } 840 switch (msg.what) { 841 case MSG_CONNECTED_CHANGED: 842 mListener.onConnectedChanged(); 843 break; 844 case MSG_WIFI_STATE_CHANGED: 845 mListener.onWifiStateChanged(msg.arg1); 846 break; 847 case MSG_ACCESS_POINT_CHANGED: 848 // Only notify listeners of changes if we have fresh scan results, otherwise the 849 // UI will be updated with stale results. We want to copy the APs regardless, 850 // for instances where forceUpdate was invoked by the caller. 851 if (mStaleScanResults) { 852 copyAndNotifyListeners(false /*notifyListeners*/); 853 } else { 854 copyAndNotifyListeners(true /*notifyListeners*/); 855 mListener.onAccessPointsChanged(); 856 } 857 break; 858 case MSG_RESUME_SCANNING: 859 if (mScanner != null) { 860 mScanner.resume(); 861 } 862 break; 863 case MSG_PAUSE_SCANNING: 864 if (mScanner != null) { 865 mScanner.pause(); 866 } 867 synchronized (mLock) { 868 mStaleScanResults = true; 869 } 870 break; 871 } 872 } 873 removePendingMessages()874 void removePendingMessages() { 875 removeMessages(MSG_ACCESS_POINT_CHANGED); 876 removeMessages(MSG_CONNECTED_CHANGED); 877 removeMessages(MSG_WIFI_STATE_CHANGED); 878 removeMessages(MSG_PAUSE_SCANNING); 879 removeMessages(MSG_RESUME_SCANNING); 880 } 881 } 882 883 @VisibleForTesting 884 final class WorkHandler extends Handler { 885 private static final int MSG_UPDATE_ACCESS_POINTS = 0; 886 private static final int MSG_UPDATE_NETWORK_INFO = 1; 887 private static final int MSG_RESUME = 2; 888 private static final int MSG_UPDATE_WIFI_STATE = 3; 889 890 private static final int CLEAR_STALE_SCAN_RESULTS = 1; 891 WorkHandler(Looper looper)892 public WorkHandler(Looper looper) { 893 super(looper); 894 } 895 896 @Override handleMessage(Message msg)897 public void handleMessage(Message msg) { 898 synchronized (mLock) { 899 processMessage(msg); 900 } 901 } 902 processMessage(Message msg)903 private void processMessage(Message msg) { 904 if (!mRegistered) return; 905 906 switch (msg.what) { 907 case MSG_UPDATE_ACCESS_POINTS: 908 if (msg.arg1 == CLEAR_STALE_SCAN_RESULTS) { 909 mStaleScanResults = false; 910 } 911 updateAccessPoints(); 912 break; 913 case MSG_UPDATE_NETWORK_INFO: 914 updateNetworkInfo((NetworkInfo) msg.obj); 915 break; 916 case MSG_RESUME: 917 handleResume(); 918 break; 919 case MSG_UPDATE_WIFI_STATE: 920 if (msg.arg1 == WifiManager.WIFI_STATE_ENABLED) { 921 if (mScanner != null) { 922 // We only need to resume if mScanner isn't null because 923 // that means we want to be scanning. 924 mScanner.resume(); 925 } 926 } else { 927 mLastInfo = null; 928 mLastNetworkInfo = null; 929 if (mScanner != null) { 930 mScanner.pause(); 931 } 932 synchronized (mLock) { 933 mStaleScanResults = true; 934 } 935 } 936 mMainHandler.obtainMessage(MainHandler.MSG_WIFI_STATE_CHANGED, msg.arg1, 0) 937 .sendToTarget(); 938 break; 939 } 940 } 941 removePendingMessages()942 private void removePendingMessages() { 943 removeMessages(MSG_UPDATE_ACCESS_POINTS); 944 removeMessages(MSG_UPDATE_NETWORK_INFO); 945 removeMessages(MSG_RESUME); 946 removeMessages(MSG_UPDATE_WIFI_STATE); 947 } 948 } 949 950 @VisibleForTesting 951 class Scanner extends Handler { 952 static final int MSG_SCAN = 0; 953 954 private int mRetry = 0; 955 resume()956 void resume() { 957 if (!hasMessages(MSG_SCAN)) { 958 sendEmptyMessage(MSG_SCAN); 959 } 960 } 961 forceScan()962 void forceScan() { 963 removeMessages(MSG_SCAN); 964 sendEmptyMessage(MSG_SCAN); 965 } 966 pause()967 void pause() { 968 mRetry = 0; 969 removeMessages(MSG_SCAN); 970 } 971 972 @VisibleForTesting isScanning()973 boolean isScanning() { 974 return hasMessages(MSG_SCAN); 975 } 976 977 @Override handleMessage(Message message)978 public void handleMessage(Message message) { 979 if (message.what != MSG_SCAN) return; 980 if (mWifiManager.startScan()) { 981 mRetry = 0; 982 } else if (++mRetry >= 3) { 983 mRetry = 0; 984 if (mContext != null) { 985 Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show(); 986 } 987 return; 988 } 989 sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS); 990 } 991 } 992 993 /** A restricted multimap for use in constructAccessPoints */ 994 private static class Multimap<K,V> { 995 private final HashMap<K,List<V>> store = new HashMap<K,List<V>>(); 996 /** retrieve a non-null list of values with key K */ getAll(K key)997 List<V> getAll(K key) { 998 List<V> values = store.get(key); 999 return values != null ? values : Collections.<V>emptyList(); 1000 } 1001 put(K key, V val)1002 void put(K key, V val) { 1003 List<V> curVals = store.get(key); 1004 if (curVals == null) { 1005 curVals = new ArrayList<V>(3); 1006 store.put(key, curVals); 1007 } 1008 curVals.add(val); 1009 } 1010 } 1011 1012 public interface WifiListener { 1013 /** 1014 * Called when the state of Wifi has changed, the state will be one of 1015 * the following. 1016 * 1017 * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li> 1018 * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li> 1019 * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li> 1020 * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li> 1021 * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li> 1022 * <p> 1023 * 1024 * @param state The new state of wifi. 1025 */ onWifiStateChanged(int state)1026 void onWifiStateChanged(int state); 1027 1028 /** 1029 * Called when the connection state of wifi has changed and isConnected 1030 * should be called to get the updated state. 1031 */ onConnectedChanged()1032 void onConnectedChanged(); 1033 1034 /** 1035 * Called to indicate the list of AccessPoints has been updated and 1036 * getAccessPoints should be called to get the latest information. 1037 */ onAccessPointsChanged()1038 void onAccessPointsChanged(); 1039 } 1040 1041 /** 1042 * Helps capture notifications that were generated during AccessPoint modification. Used later 1043 * on by {@link #copyAndNotifyListeners(boolean)} to send notifications. 1044 */ 1045 private static class AccessPointListenerAdapter implements AccessPoint.AccessPointListener { 1046 static final int AP_CHANGED = 1; 1047 static final int LEVEL_CHANGED = 2; 1048 1049 final SparseIntArray mPendingNotifications = new SparseIntArray(); 1050 1051 @Override onAccessPointChanged(AccessPoint accessPoint)1052 public void onAccessPointChanged(AccessPoint accessPoint) { 1053 int type = mPendingNotifications.get(accessPoint.mId); 1054 mPendingNotifications.put(accessPoint.mId, type | AP_CHANGED); 1055 } 1056 1057 @Override onLevelChanged(AccessPoint accessPoint)1058 public void onLevelChanged(AccessPoint accessPoint) { 1059 int type = mPendingNotifications.get(accessPoint.mId); 1060 mPendingNotifications.put(accessPoint.mId, type | LEVEL_CHANGED); 1061 } 1062 } 1063 1064 /** 1065 * Responsible for copying access points from {@link #mInternalAccessPoints} and notifying 1066 * accesspoint listeners. 1067 * 1068 * @param notifyListeners if true, accesspoint listeners are notified, otherwise notifications 1069 * dropped. 1070 */ 1071 @MainThread copyAndNotifyListeners(boolean notifyListeners)1072 private void copyAndNotifyListeners(boolean notifyListeners) { 1073 // Need to watch out for memory allocations on main thread. 1074 SparseArray<AccessPoint> oldAccessPoints = new SparseArray<>(); 1075 SparseIntArray notificationMap = null; 1076 List<AccessPoint> updatedAccessPoints = new ArrayList<>(); 1077 1078 for (AccessPoint accessPoint : mAccessPoints) { 1079 oldAccessPoints.put(accessPoint.mId, accessPoint); 1080 } 1081 1082 synchronized (mLock) { 1083 if (DBG()) { 1084 Log.d(TAG, "Starting to copy AP items on the MainHandler. Internal APs: " 1085 + mInternalAccessPoints); 1086 } 1087 1088 if (notifyListeners) { 1089 notificationMap = mAccessPointListenerAdapter.mPendingNotifications.clone(); 1090 } 1091 1092 mAccessPointListenerAdapter.mPendingNotifications.clear(); 1093 1094 for (AccessPoint internalAccessPoint : mInternalAccessPoints) { 1095 AccessPoint accessPoint = oldAccessPoints.get(internalAccessPoint.mId); 1096 if (accessPoint == null) { 1097 accessPoint = new AccessPoint(mContext, internalAccessPoint); 1098 } else { 1099 accessPoint.copyFrom(internalAccessPoint); 1100 } 1101 updatedAccessPoints.add(accessPoint); 1102 } 1103 } 1104 1105 mAccessPoints.clear(); 1106 mAccessPoints.addAll(updatedAccessPoints); 1107 1108 if (notificationMap != null && notificationMap.size() > 0) { 1109 for (AccessPoint accessPoint : updatedAccessPoints) { 1110 int notificationType = notificationMap.get(accessPoint.mId); 1111 AccessPoint.AccessPointListener listener = accessPoint.mAccessPointListener; 1112 if (notificationType == 0 || listener == null) { 1113 continue; 1114 } 1115 1116 if ((notificationType & AccessPointListenerAdapter.AP_CHANGED) != 0) { 1117 listener.onAccessPointChanged(accessPoint); 1118 } 1119 1120 if ((notificationType & AccessPointListenerAdapter.LEVEL_CHANGED) != 0) { 1121 listener.onLevelChanged(accessPoint); 1122 } 1123 } 1124 } 1125 } 1126 } 1127