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.NetworkInfo;
23import android.net.NetworkInfo.DetailedState;
24import android.net.wifi.ScanResult;
25import android.net.wifi.WifiConfiguration;
26import android.net.wifi.WifiInfo;
27import android.net.wifi.WifiManager;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.Message;
31import android.util.Log;
32import android.widget.Toast;
33
34import com.android.internal.annotations.VisibleForTesting;
35import com.android.settingslib.R;
36
37import java.io.PrintWriter;
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.Collections;
41import java.util.HashMap;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Map;
45import java.util.concurrent.atomic.AtomicBoolean;
46
47/**
48 * Tracks saved or available wifi networks and their state.
49 */
50public 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