Home | History | Annotate | Download | only in wifi
      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