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.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";
     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 
    158     public WifiTracker(Context context, WifiListener wifiListener,
    159             boolean includeSaved, boolean includeScans) {
    160         this(context, wifiListener, null, includeSaved, includeScans);
    161     }
    162 
    163     public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
    164             boolean includeSaved, boolean includeScans) {
    165         this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
    166     }
    167 
    168     public WifiTracker(Context context, WifiListener wifiListener,
    169             boolean includeSaved, boolean includeScans, boolean includePasspoints) {
    170         this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
    171     }
    172 
    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
    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
    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      */
    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      */
    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      */
    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
    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 
    335     private void registerScoreCache() {
    336         mNetworkScoreManager.registerNetworkScoreCache(
    337                 NetworkKey.TYPE_WIFI,
    338                 mScoreCache,
    339                 NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
    340     }
    341 
    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
    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 
    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
    399     public List<AccessPoint> getAccessPoints() {
    400         return new ArrayList<>(mAccessPoints);
    401     }
    402 
    403     public WifiManager getManager() {
    404         return mWifiManager;
    405     }
    406 
    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      */
    417     public int getNumSavedNetworks() {
    418         return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size();
    419     }
    420 
    421     public boolean isConnected() {
    422         return mConnected.get();
    423     }
    424 
    425     public void dump(PrintWriter pw) {
    426         pw.println("  - wifi tracker ------");
    427         for (AccessPoint accessPoint : getAccessPoints()) {
    428             pw.println("  " + accessPoint);
    429         }
    430     }
    431 
    432     private void handleResume() {
    433         mScanResultCache.clear();
    434         mSeenBssids.clear();
    435         mScanId = 0;
    436     }
    437 
    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 
    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      */
    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")
    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
    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
    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 
    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 
    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      */
    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 
    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 
    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 {
    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 
    831         public MainHandler(Looper looper) {
    832             super(looper);
    833         }
    834 
    835         @Override
    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 
    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 
    892         public WorkHandler(Looper looper) {
    893             super(looper);
    894         }
    895 
    896         @Override
    897         public void handleMessage(Message msg) {
    898             synchronized (mLock) {
    899                 processMessage(msg);
    900             }
    901         }
    902 
    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 
    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 
    956         void resume() {
    957             if (!hasMessages(MSG_SCAN)) {
    958                 sendEmptyMessage(MSG_SCAN);
    959             }
    960         }
    961 
    962         void forceScan() {
    963             removeMessages(MSG_SCAN);
    964             sendEmptyMessage(MSG_SCAN);
    965         }
    966 
    967         void pause() {
    968             mRetry = 0;
    969             removeMessages(MSG_SCAN);
    970         }
    971 
    972         @VisibleForTesting
    973         boolean isScanning() {
    974             return hasMessages(MSG_SCAN);
    975         }
    976 
    977         @Override
    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 */
    997         List<V> getAll(K key) {
    998             List<V> values = store.get(key);
    999             return values != null ? values : Collections.<V>emptyList();
   1000         }
   1001 
   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          */
   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          */
   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          */
   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
   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
   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
   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