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 */
16package com.android.settingslib.wifi;
17
18import android.content.BroadcastReceiver;
19import android.content.Context;
20import android.content.Intent;
21import android.content.IntentFilter;
22import android.net.ConnectivityManager;
23import android.net.Network;
24import android.net.NetworkCapabilities;
25import android.net.NetworkInfo;
26import android.net.NetworkInfo.DetailedState;
27import android.net.NetworkRequest;
28import android.net.wifi.ScanResult;
29import android.net.wifi.WifiConfiguration;
30import android.net.wifi.WifiInfo;
31import android.net.wifi.WifiManager;
32import android.os.Handler;
33import android.os.Looper;
34import android.os.Message;
35import android.util.Log;
36import android.widget.Toast;
37
38import com.android.internal.annotations.VisibleForTesting;
39import com.android.settingslib.R;
40
41import java.io.PrintWriter;
42import java.util.ArrayList;
43import java.util.Collection;
44import java.util.Collections;
45import java.util.HashMap;
46import java.util.Iterator;
47import java.util.List;
48import java.util.Map;
49import java.util.concurrent.atomic.AtomicBoolean;
50
51/**
52 * Tracks saved or available wifi networks and their state.
53 */
54public 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
96    public WifiTracker(Context context, WifiListener wifiListener,
97            boolean includeSaved, boolean includeScans) {
98        this(context, wifiListener, null, includeSaved, includeScans);
99    }
100
101    public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
102            boolean includeSaved, boolean includeScans) {
103        this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
104    }
105
106    public WifiTracker(Context context, WifiListener wifiListener,
107            boolean includeSaved, boolean includeScans, boolean includePasspoints) {
108        this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
109    }
110
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
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     */
162    public void forceUpdate() {
163        updateAccessPoints();
164    }
165
166    /**
167     * Force a scan for wifi networks to happen now.
168     */
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     */
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     */
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     */
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     */
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     */
236    public List<AccessPoint> getAccessPoints() {
237        synchronized (mAccessPoints) {
238            return new ArrayList<>(mAccessPoints);
239        }
240    }
241
242    public WifiManager getManager() {
243        return mWifiManager;
244    }
245
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     */
254    public boolean doSavedNetworksExist() {
255        return mSavedNetworksExist;
256    }
257
258    public boolean isConnected() {
259        return mConnected.get();
260    }
261
262    public void dump(PrintWriter pw) {
263        pw.println("  - wifi tracker ------");
264        for (AccessPoint accessPoint : getAccessPoints()) {
265            pw.println("  " + accessPoint);
266        }
267    }
268
269    private void handleResume() {
270        mScanResultCache.clear();
271        mSeenBssids.clear();
272        mScanId = 0;
273    }
274
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
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
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
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
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
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
506    private void updateWifiState(int state) {
507        mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_WIFI_STATE, state, 0).sendToTarget();
508    }
509
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 {
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
562        public MainHandler(Looper looper) {
563            super(looper);
564        }
565
566        @Override
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
601        public WorkHandler(Looper looper) {
602            super(looper);
603        }
604
605        @Override
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
644        void resume() {
645            if (!hasMessages(MSG_SCAN)) {
646                sendEmptyMessage(MSG_SCAN);
647            }
648        }
649
650        void forceScan() {
651            removeMessages(MSG_SCAN);
652            sendEmptyMessage(MSG_SCAN);
653        }
654
655        void pause() {
656            mRetry = 0;
657            removeMessages(MSG_SCAN);
658        }
659
660        @VisibleForTesting
661        boolean isScanning() {
662            return hasMessages(MSG_SCAN);
663        }
664
665        @Override
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 */
685        List<V> getAll(K key) {
686            List<V> values = store.get(key);
687            return values != null ? values : Collections.<V>emptyList();
688        }
689
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         */
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         */
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         */
726        void onAccessPointsChanged();
727    }
728}
729