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 
WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans)89     public WifiTracker(Context context, WifiListener wifiListener,
90             boolean includeSaved, boolean includeScans) {
91         this(context, wifiListener, null, includeSaved, includeScans);
92     }
93 
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans)94     public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
95             boolean includeSaved, boolean includeScans) {
96         this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
97     }
98 
WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans, boolean includePasspoints)99     public WifiTracker(Context context, WifiListener wifiListener,
100             boolean includeSaved, boolean includeScans, boolean includePasspoints) {
101         this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
102     }
103 
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints)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
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints, WifiManager wifiManager, Looper currentLooper)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      */
forceUpdate()148     public void forceUpdate() {
149         updateAccessPoints();
150     }
151 
152     /**
153      * Force a scan for wifi networks to happen now.
154      */
forceScan()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      */
pauseScanning()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      */
resumeScanning()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      */
startTracking()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      */
stopTracking()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      */
getAccessPoints()218     public List<AccessPoint> getAccessPoints() {
219         synchronized (mAccessPoints) {
220             return new ArrayList<>(mAccessPoints);
221         }
222     }
223 
getManager()224     public WifiManager getManager() {
225         return mWifiManager;
226     }
227 
isWifiEnabled()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      */
doSavedNetworksExist()236     public boolean doSavedNetworksExist() {
237         return mSavedNetworksExist;
238     }
239 
isConnected()240     public boolean isConnected() {
241         return mConnected.get();
242     }
243 
dump(PrintWriter pw)244     public void dump(PrintWriter pw) {
245         pw.println("  - wifi tracker ------");
246         for (AccessPoint accessPoint : getAccessPoints()) {
247             pw.println("  " + accessPoint);
248         }
249     }
250 
handleResume()251     private void handleResume() {
252         mScanResultCache.clear();
253         mSeenBssids.clear();
254         mScanId = 0;
255     }
256 
fetchScanResults()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 
getWifiConfigurationForNetworkId(int networkId)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 
updateAccessPoints()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 
getCachedOrCreate(ScanResult result, List<AccessPoint> cache)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 
getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache)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 
updateNetworkInfo(NetworkInfo networkInfo)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 
updateWifiState(int state)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 
getCurrentAccessPoints(Context context, boolean includeSaved, boolean includeScans, boolean includePasspoints)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 
MainHandler(Looper looper)532         public MainHandler(Looper looper) {
533             super(looper);
534         }
535 
536         @Override
handleMessage(Message msg)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 
WorkHandler(Looper looper)570         public WorkHandler(Looper looper) {
571             super(looper);
572         }
573 
574         @Override
handleMessage(Message msg)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 
resume()596         void resume() {
597             if (!hasMessages(MSG_SCAN)) {
598                 sendEmptyMessage(MSG_SCAN);
599             }
600         }
601 
forceScan()602         void forceScan() {
603             removeMessages(MSG_SCAN);
604             sendEmptyMessage(MSG_SCAN);
605         }
606 
pause()607         void pause() {
608             mRetry = 0;
609             removeMessages(MSG_SCAN);
610         }
611 
612         @VisibleForTesting
isScanning()613         boolean isScanning() {
614             return hasMessages(MSG_SCAN);
615         }
616 
617         @Override
handleMessage(Message message)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 */
getAll(K key)637         List<V> getAll(K key) {
638             List<V> values = store.get(key);
639             return values != null ? values : Collections.<V>emptyList();
640         }
641 
put(K key, V val)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          */
onWifiStateChanged(int state)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          */
onConnectedChanged()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          */
onAccessPointsChanged()678         void onAccessPointsChanged();
679     }
680 }
681