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.content.BroadcastReceiver;
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.content.IntentFilter;
     22 import android.net.NetworkInfo;
     23 import android.net.NetworkInfo.DetailedState;
     24 import android.net.wifi.ScanResult;
     25 import android.net.wifi.WifiConfiguration;
     26 import android.net.wifi.WifiInfo;
     27 import android.net.wifi.WifiManager;
     28 import android.os.Handler;
     29 import android.os.Looper;
     30 import android.os.Message;
     31 import android.util.Log;
     32 import android.widget.Toast;
     33 
     34 import com.android.internal.annotations.VisibleForTesting;
     35 import com.android.settingslib.R;
     36 
     37 import java.io.PrintWriter;
     38 import java.util.ArrayList;
     39 import java.util.Collection;
     40 import java.util.Collections;
     41 import java.util.HashMap;
     42 import java.util.Iterator;
     43 import java.util.List;
     44 import java.util.Map;
     45 import java.util.concurrent.atomic.AtomicBoolean;
     46 
     47 /**
     48  * Tracks saved or available wifi networks and their state.
     49  */
     50 public class WifiTracker {
     51     private static final String TAG = "WifiTracker";
     52     private static final boolean DBG = false;
     53 
     54     /** verbose logging flag. this flag is set thru developer debugging options
     55      * and used so as to assist with in-the-field WiFi connectivity debugging  */
     56     public static int sVerboseLogging = 0;
     57 
     58     // TODO: Allow control of this?
     59     // Combo scans can take 5-6s to complete - set to 10s.
     60     private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;
     61 
     62     private final Context mContext;
     63     private final WifiManager mWifiManager;
     64     private final IntentFilter mFilter;
     65 
     66     private final AtomicBoolean mConnected = new AtomicBoolean(false);
     67     private final WifiListener mListener;
     68     private final boolean mIncludeSaved;
     69     private final boolean mIncludeScans;
     70     private final boolean mIncludePasspoints;
     71 
     72     private final MainHandler mMainHandler;
     73     private final WorkHandler mWorkHandler;
     74 
     75     private boolean mSavedNetworksExist;
     76     private boolean mRegistered;
     77     private ArrayList<AccessPoint> mAccessPoints = new ArrayList<>();
     78     private HashMap<String, Integer> mSeenBssids = new HashMap<>();
     79     private HashMap<String, ScanResult> mScanResultCache = new HashMap<>();
     80     private Integer mScanId = 0;
     81     private static final int NUM_SCANS_TO_CONFIRM_AP_LOSS = 3;
     82 
     83     private NetworkInfo mLastNetworkInfo;
     84     private WifiInfo mLastInfo;
     85 
     86     @VisibleForTesting
     87     Scanner mScanner;
     88 
     89     public WifiTracker(Context context, WifiListener wifiListener,
     90             boolean includeSaved, boolean includeScans) {
     91         this(context, wifiListener, null, includeSaved, includeScans);
     92     }
     93 
     94     public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
     95             boolean includeSaved, boolean includeScans) {
     96         this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
     97     }
     98 
     99     public WifiTracker(Context context, WifiListener wifiListener,
    100             boolean includeSaved, boolean includeScans, boolean includePasspoints) {
    101         this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
    102     }
    103 
    104     public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
    105             boolean includeSaved, boolean includeScans, boolean includePasspoints) {
    106         this(context, wifiListener, workerLooper, includeSaved, includeScans, includePasspoints,
    107                 (WifiManager) context.getSystemService(Context.WIFI_SERVICE), Looper.myLooper());
    108     }
    109 
    110     @VisibleForTesting
    111     WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
    112             boolean includeSaved, boolean includeScans, boolean includePasspoints,
    113             WifiManager wifiManager, Looper currentLooper) {
    114         if (!includeSaved && !includeScans) {
    115             throw new IllegalArgumentException("Must include either saved or scans");
    116         }
    117         mContext = context;
    118         if (currentLooper == null) {
    119             // When we aren't on a looper thread, default to the main.
    120             currentLooper = Looper.getMainLooper();
    121         }
    122         mMainHandler = new MainHandler(currentLooper);
    123         mWorkHandler = new WorkHandler(
    124                 workerLooper != null ? workerLooper : currentLooper);
    125         mWifiManager = wifiManager;
    126         mIncludeSaved = includeSaved;
    127         mIncludeScans = includeScans;
    128         mIncludePasspoints = includePasspoints;
    129         mListener = wifiListener;
    130 
    131         // check if verbose logging has been turned on or off
    132         sVerboseLogging = mWifiManager.getVerboseLoggingLevel();
    133 
    134         mFilter = new IntentFilter();
    135         mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
    136         mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
    137         mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
    138         mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
    139         mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
    140         mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
    141         mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
    142         mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
    143     }
    144 
    145     /**
    146      * Forces an update of the wifi networks when not scanning.
    147      */
    148     public void forceUpdate() {
    149         updateAccessPoints();
    150     }
    151 
    152     /**
    153      * Force a scan for wifi networks to happen now.
    154      */
    155     public void forceScan() {
    156         if (mWifiManager.isWifiEnabled() && mScanner != null) {
    157             mScanner.forceScan();
    158         }
    159     }
    160 
    161     /**
    162      * Temporarily stop scanning for wifi networks.
    163      */
    164     public void pauseScanning() {
    165         if (mScanner != null) {
    166             mScanner.pause();
    167             mScanner = null;
    168         }
    169     }
    170 
    171     /**
    172      * Resume scanning for wifi networks after it has been paused.
    173      */
    174     public void resumeScanning() {
    175         if (mScanner == null) {
    176             mScanner = new Scanner();
    177         }
    178 
    179         mWorkHandler.sendEmptyMessage(WorkHandler.MSG_RESUME);
    180         if (mWifiManager.isWifiEnabled()) {
    181             mScanner.resume();
    182         }
    183         mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
    184     }
    185 
    186     /**
    187      * Start tracking wifi networks.
    188      * Registers listeners and starts scanning for wifi networks. If this is not called
    189      * then forceUpdate() must be called to populate getAccessPoints().
    190      */
    191     public void startTracking() {
    192         resumeScanning();
    193         if (!mRegistered) {
    194             mContext.registerReceiver(mReceiver, mFilter);
    195             mRegistered = true;
    196         }
    197     }
    198 
    199     /**
    200      * Stop tracking wifi networks.
    201      * Unregisters all listeners and stops scanning for wifi networks. This should always
    202      * be called when done with a WifiTracker (if startTracking was called) to ensure
    203      * proper cleanup.
    204      */
    205     public void stopTracking() {
    206         if (mRegistered) {
    207             mWorkHandler.removeMessages(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
    208             mWorkHandler.removeMessages(WorkHandler.MSG_UPDATE_NETWORK_INFO);
    209             mContext.unregisterReceiver(mReceiver);
    210             mRegistered = false;
    211         }
    212         pauseScanning();
    213     }
    214 
    215     /**
    216      * Gets the current list of access points.
    217      */
    218     public List<AccessPoint> getAccessPoints() {
    219         synchronized (mAccessPoints) {
    220             return new ArrayList<>(mAccessPoints);
    221         }
    222     }
    223 
    224     public WifiManager getManager() {
    225         return mWifiManager;
    226     }
    227 
    228     public boolean isWifiEnabled() {
    229         return mWifiManager.isWifiEnabled();
    230     }
    231 
    232     /**
    233      * @return true when there are saved networks on the device, regardless
    234      * of whether the WifiTracker is tracking saved networks.
    235      */
    236     public boolean doSavedNetworksExist() {
    237         return mSavedNetworksExist;
    238     }
    239 
    240     public boolean isConnected() {
    241         return mConnected.get();
    242     }
    243 
    244     public void dump(PrintWriter pw) {
    245         pw.println("  - wifi tracker ------");
    246         for (AccessPoint accessPoint : getAccessPoints()) {
    247             pw.println("  " + accessPoint);
    248         }
    249     }
    250 
    251     private void handleResume() {
    252         mScanResultCache.clear();
    253         mSeenBssids.clear();
    254         mScanId = 0;
    255     }
    256 
    257     private Collection<ScanResult> fetchScanResults() {
    258         mScanId++;
    259         final List<ScanResult> newResults = mWifiManager.getScanResults();
    260         for (ScanResult newResult : newResults) {
    261             mScanResultCache.put(newResult.BSSID, newResult);
    262             mSeenBssids.put(newResult.BSSID, mScanId);
    263         }
    264 
    265         if (mScanId > NUM_SCANS_TO_CONFIRM_AP_LOSS) {
    266             if (DBG) Log.d(TAG, "------ Dumping SSIDs that were expired on this scan ------");
    267             Integer threshold = mScanId - NUM_SCANS_TO_CONFIRM_AP_LOSS;
    268             for (Iterator<Map.Entry<String, Integer>> it = mSeenBssids.entrySet().iterator();
    269                     it.hasNext(); /* nothing */) {
    270                 Map.Entry<String, Integer> e = it.next();
    271                 if (e.getValue() < threshold) {
    272                     ScanResult result = mScanResultCache.get(e.getKey());
    273                     if (DBG) Log.d(TAG, "Removing " + e.getKey() + ":(" + result.SSID + ")");
    274                     mScanResultCache.remove(e.getKey());
    275                     it.remove();
    276                 }
    277             }
    278             if (DBG) Log.d(TAG, "---- Done Dumping SSIDs that were expired on this scan ----");
    279         }
    280 
    281         return mScanResultCache.values();
    282     }
    283 
    284     private WifiConfiguration getWifiConfigurationForNetworkId(int networkId) {
    285         final List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
    286         if (configs != null) {
    287             for (WifiConfiguration config : configs) {
    288                 if (mLastInfo != null && networkId == config.networkId &&
    289                         !(config.selfAdded && config.numAssociation == 0)) {
    290                     return config;
    291                 }
    292             }
    293         }
    294         return null;
    295     }
    296 
    297     private void updateAccessPoints() {
    298         // Swap the current access points into a cached list.
    299         List<AccessPoint> cachedAccessPoints = getAccessPoints();
    300         ArrayList<AccessPoint> accessPoints = new ArrayList<>();
    301 
    302         // Clear out the configs so we don't think something is saved when it isn't.
    303         for (AccessPoint accessPoint : cachedAccessPoints) {
    304             accessPoint.clearConfig();
    305         }
    306 
    307         /** Lookup table to more quickly update AccessPoints by only considering objects with the
    308          * correct SSID.  Maps SSID -> List of AccessPoints with the given SSID.  */
    309         Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>();
    310         WifiConfiguration connectionConfig = null;
    311         if (mLastInfo != null) {
    312             connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId());
    313         }
    314 
    315         final List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
    316         if (configs != null) {
    317             mSavedNetworksExist = configs.size() != 0;
    318             for (WifiConfiguration config : configs) {
    319                 if (config.selfAdded && config.numAssociation == 0) {
    320                     continue;
    321                 }
    322                 AccessPoint accessPoint = getCachedOrCreate(config, cachedAccessPoints);
    323                 if (mLastInfo != null && mLastNetworkInfo != null) {
    324                     if (config.isPasspoint() == false) {
    325                         accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
    326                     }
    327                 }
    328                 if (mIncludeSaved) {
    329                     if (!config.isPasspoint() || mIncludePasspoints)
    330                         accessPoints.add(accessPoint);
    331 
    332                     if (config.isPasspoint() == false) {
    333                         apMap.put(accessPoint.getSsidStr(), accessPoint);
    334                     }
    335                 } else {
    336                     // If we aren't using saved networks, drop them into the cache so that
    337                     // we have access to their saved info.
    338                     cachedAccessPoints.add(accessPoint);
    339                 }
    340             }
    341         }
    342 
    343         final Collection<ScanResult> results = fetchScanResults();
    344         if (results != null) {
    345             for (ScanResult result : results) {
    346                 // Ignore hidden and ad-hoc networks.
    347                 if (result.SSID == null || result.SSID.length() == 0 ||
    348                         result.capabilities.contains("[IBSS]")) {
    349                     continue;
    350                 }
    351 
    352                 boolean found = false;
    353                 for (AccessPoint accessPoint : apMap.getAll(result.SSID)) {
    354                     if (accessPoint.update(result)) {
    355                         found = true;
    356                         break;
    357                     }
    358                 }
    359                 if (!found && mIncludeScans) {
    360                     AccessPoint accessPoint = getCachedOrCreate(result, cachedAccessPoints);
    361                     if (mLastInfo != null && mLastNetworkInfo != null) {
    362                         accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
    363                     }
    364 
    365                     if (result.isPasspointNetwork()) {
    366                         WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result);
    367                         if (config != null) {
    368                             accessPoint.update(config);
    369                         }
    370                     }
    371 
    372                     if (mLastInfo != null && mLastInfo.getBSSID() != null
    373                             && mLastInfo.getBSSID().equals(result.BSSID)
    374                             && connectionConfig != null && connectionConfig.isPasspoint()) {
    375                         /* This network is connected via this passpoint config */
    376                         /* SSID match is not going to work for it; so update explicitly */
    377                         accessPoint.update(connectionConfig);
    378                     }
    379 
    380                     accessPoints.add(accessPoint);
    381                     apMap.put(accessPoint.getSsidStr(), accessPoint);
    382                 }
    383             }
    384         }
    385 
    386         // Pre-sort accessPoints to speed preference insertion
    387         Collections.sort(accessPoints);
    388 
    389         // Log accesspoints that were deleted
    390         if (DBG) Log.d(TAG, "------ Dumping SSIDs that were not seen on this scan ------");
    391         for (AccessPoint prevAccessPoint : mAccessPoints) {
    392             if (prevAccessPoint.getSsid() == null) continue;
    393             String prevSsid = prevAccessPoint.getSsidStr();
    394             boolean found = false;
    395             for (AccessPoint newAccessPoint : accessPoints) {
    396                 if (newAccessPoint.getSsid() != null && newAccessPoint.getSsid().equals(prevSsid)) {
    397                     found = true;
    398                     break;
    399                 }
    400             }
    401             if (!found)
    402                 if (DBG) Log.d(TAG, "Did not find " + prevSsid + " in this scan");
    403         }
    404         if (DBG)  Log.d(TAG, "---- Done dumping SSIDs that were not seen on this scan ----");
    405 
    406         mAccessPoints = accessPoints;
    407         mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
    408     }
    409 
    410     private AccessPoint getCachedOrCreate(ScanResult result, List<AccessPoint> cache) {
    411         final int N = cache.size();
    412         for (int i = 0; i < N; i++) {
    413             if (cache.get(i).matches(result)) {
    414                 AccessPoint ret = cache.remove(i);
    415                 ret.update(result);
    416                 return ret;
    417             }
    418         }
    419         return new AccessPoint(mContext, result);
    420     }
    421 
    422     private AccessPoint getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache) {
    423         final int N = cache.size();
    424         for (int i = 0; i < N; i++) {
    425             if (cache.get(i).matches(config)) {
    426                 AccessPoint ret = cache.remove(i);
    427                 ret.loadConfig(config);
    428                 return ret;
    429             }
    430         }
    431         return new AccessPoint(mContext, config);
    432     }
    433 
    434     private void updateNetworkInfo(NetworkInfo networkInfo) {
    435         /* sticky broadcasts can call this when wifi is disabled */
    436         if (!mWifiManager.isWifiEnabled()) {
    437             mMainHandler.sendEmptyMessage(MainHandler.MSG_PAUSE_SCANNING);
    438             return;
    439         }
    440 
    441         if (networkInfo != null &&
    442                 networkInfo.getDetailedState() == DetailedState.OBTAINING_IPADDR) {
    443             mMainHandler.sendEmptyMessage(MainHandler.MSG_PAUSE_SCANNING);
    444         } else {
    445             mMainHandler.sendEmptyMessage(MainHandler.MSG_RESUME_SCANNING);
    446         }
    447 
    448         mLastInfo = mWifiManager.getConnectionInfo();
    449         if (networkInfo != null) {
    450             mLastNetworkInfo = networkInfo;
    451         }
    452 
    453         WifiConfiguration connectionConfig = null;
    454         if (mLastInfo != null) {
    455             connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId());
    456         }
    457 
    458         boolean reorder = false;
    459         for (int i = mAccessPoints.size() - 1; i >= 0; --i) {
    460             if (mAccessPoints.get(i).update(connectionConfig, mLastInfo, mLastNetworkInfo)) {
    461                 reorder = true;
    462             }
    463         }
    464         if (reorder) {
    465             synchronized (mAccessPoints) {
    466                 Collections.sort(mAccessPoints);
    467             }
    468             mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
    469         }
    470     }
    471 
    472     private void updateWifiState(int state) {
    473         if (state == WifiManager.WIFI_STATE_ENABLED) {
    474             if (mScanner != null) {
    475                 // We only need to resume if mScanner isn't null because
    476                 // that means we want to be scanning.
    477                 mScanner.resume();
    478             }
    479         } else {
    480             mLastInfo = null;
    481             mLastNetworkInfo = null;
    482             if (mScanner != null) {
    483                 mScanner.pause();
    484             }
    485         }
    486         mMainHandler.obtainMessage(MainHandler.MSG_WIFI_STATE_CHANGED, state, 0).sendToTarget();
    487     }
    488 
    489     public static List<AccessPoint> getCurrentAccessPoints(Context context, boolean includeSaved,
    490             boolean includeScans, boolean includePasspoints) {
    491         WifiTracker tracker = new WifiTracker(context,
    492                 null, null, includeSaved, includeScans, includePasspoints);
    493         tracker.forceUpdate();
    494         return tracker.getAccessPoints();
    495     }
    496 
    497     @VisibleForTesting
    498     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    499         @Override
    500         public void onReceive(Context context, Intent intent) {
    501             String action = intent.getAction();
    502             if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
    503                 updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
    504                         WifiManager.WIFI_STATE_UNKNOWN));
    505             } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action) ||
    506                     WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) ||
    507                     WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
    508                 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
    509             } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
    510                 NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
    511                         WifiManager.EXTRA_NETWORK_INFO);
    512                 mConnected.set(info.isConnected());
    513 
    514                 mMainHandler.sendEmptyMessage(MainHandler.MSG_CONNECTED_CHANGED);
    515 
    516                 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
    517                 mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO, info)
    518                         .sendToTarget();
    519             } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
    520                 mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO);
    521             }
    522         }
    523     };
    524 
    525     private final class MainHandler extends Handler {
    526         private static final int MSG_CONNECTED_CHANGED = 0;
    527         private static final int MSG_WIFI_STATE_CHANGED = 1;
    528         private static final int MSG_ACCESS_POINT_CHANGED = 2;
    529         private static final int MSG_RESUME_SCANNING = 3;
    530         private static final int MSG_PAUSE_SCANNING = 4;
    531 
    532         public MainHandler(Looper looper) {
    533             super(looper);
    534         }
    535 
    536         @Override
    537         public void handleMessage(Message msg) {
    538             if (mListener == null) {
    539                 return;
    540             }
    541             switch (msg.what) {
    542                 case MSG_CONNECTED_CHANGED:
    543                     mListener.onConnectedChanged();
    544                     break;
    545                 case MSG_WIFI_STATE_CHANGED:
    546                     mListener.onWifiStateChanged(msg.arg1);
    547                     break;
    548                 case MSG_ACCESS_POINT_CHANGED:
    549                     mListener.onAccessPointsChanged();
    550                     break;
    551                 case MSG_RESUME_SCANNING:
    552                     if (mScanner != null) {
    553                         mScanner.resume();
    554                     }
    555                     break;
    556                 case MSG_PAUSE_SCANNING:
    557                     if (mScanner != null) {
    558                         mScanner.pause();
    559                     }
    560                     break;
    561             }
    562         }
    563     }
    564 
    565     private final class WorkHandler extends Handler {
    566         private static final int MSG_UPDATE_ACCESS_POINTS = 0;
    567         private static final int MSG_UPDATE_NETWORK_INFO = 1;
    568         private static final int MSG_RESUME = 2;
    569 
    570         public WorkHandler(Looper looper) {
    571             super(looper);
    572         }
    573 
    574         @Override
    575         public void handleMessage(Message msg) {
    576             switch (msg.what) {
    577                 case MSG_UPDATE_ACCESS_POINTS:
    578                     updateAccessPoints();
    579                     break;
    580                 case MSG_UPDATE_NETWORK_INFO:
    581                     updateNetworkInfo((NetworkInfo) msg.obj);
    582                     break;
    583                 case MSG_RESUME:
    584                     handleResume();
    585                     break;
    586             }
    587         }
    588     }
    589 
    590     @VisibleForTesting
    591     class Scanner extends Handler {
    592         static final int MSG_SCAN = 0;
    593 
    594         private int mRetry = 0;
    595 
    596         void resume() {
    597             if (!hasMessages(MSG_SCAN)) {
    598                 sendEmptyMessage(MSG_SCAN);
    599             }
    600         }
    601 
    602         void forceScan() {
    603             removeMessages(MSG_SCAN);
    604             sendEmptyMessage(MSG_SCAN);
    605         }
    606 
    607         void pause() {
    608             mRetry = 0;
    609             removeMessages(MSG_SCAN);
    610         }
    611 
    612         @VisibleForTesting
    613         boolean isScanning() {
    614             return hasMessages(MSG_SCAN);
    615         }
    616 
    617         @Override
    618         public void handleMessage(Message message) {
    619             if (message.what != MSG_SCAN) return;
    620             if (mWifiManager.startScan()) {
    621                 mRetry = 0;
    622             } else if (++mRetry >= 3) {
    623                 mRetry = 0;
    624                 if (mContext != null) {
    625                     Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
    626                 }
    627                 return;
    628             }
    629             sendEmptyMessageDelayed(0, WIFI_RESCAN_INTERVAL_MS);
    630         }
    631     }
    632 
    633     /** A restricted multimap for use in constructAccessPoints */
    634     private static class Multimap<K,V> {
    635         private final HashMap<K,List<V>> store = new HashMap<K,List<V>>();
    636         /** retrieve a non-null list of values with key K */
    637         List<V> getAll(K key) {
    638             List<V> values = store.get(key);
    639             return values != null ? values : Collections.<V>emptyList();
    640         }
    641 
    642         void put(K key, V val) {
    643             List<V> curVals = store.get(key);
    644             if (curVals == null) {
    645                 curVals = new ArrayList<V>(3);
    646                 store.put(key, curVals);
    647             }
    648             curVals.add(val);
    649         }
    650     }
    651 
    652     public interface WifiListener {
    653         /**
    654          * Called when the state of Wifi has changed, the state will be one of
    655          * the following.
    656          *
    657          * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li>
    658          * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li>
    659          * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li>
    660          * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li>
    661          * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li>
    662          * <p>
    663          *
    664          * @param state The new state of wifi.
    665          */
    666         void onWifiStateChanged(int state);
    667 
    668         /**
    669          * Called when the connection state of wifi has changed and isConnected
    670          * should be called to get the updated state.
    671          */
    672         void onConnectedChanged();
    673 
    674         /**
    675          * Called to indicate the list of AccessPoints has been updated and
    676          * getAccessPoints should be called to get the latest information.
    677          */
    678         void onAccessPointsChanged();
    679     }
    680 }
    681