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