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.annotation.MainThread;
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.net.ConnectivityManager;
24import android.net.Network;
25import android.net.NetworkCapabilities;
26import android.net.NetworkInfo;
27import android.net.NetworkInfo.DetailedState;
28import android.net.NetworkKey;
29import android.net.NetworkRequest;
30import android.net.NetworkScoreManager;
31import android.net.ScoredNetwork;
32import android.net.wifi.ScanResult;
33import android.net.wifi.WifiConfiguration;
34import android.net.wifi.WifiInfo;
35import android.net.wifi.WifiManager;
36import android.net.wifi.WifiNetworkScoreCache;
37import android.net.wifi.WifiNetworkScoreCache.CacheListener;
38import android.os.Handler;
39import android.os.Looper;
40import android.os.Message;
41import android.provider.Settings;
42import android.support.annotation.GuardedBy;
43import android.text.format.DateUtils;
44import android.util.ArraySet;
45import android.util.Log;
46import android.util.SparseArray;
47import android.util.SparseIntArray;
48import android.widget.Toast;
49
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.settingslib.R;
52
53import java.io.PrintWriter;
54import java.util.ArrayList;
55import java.util.Collection;
56import java.util.Collections;
57import java.util.HashMap;
58import java.util.Iterator;
59import java.util.List;
60import java.util.Map;
61import java.util.Set;
62import java.util.concurrent.atomic.AtomicBoolean;
63
64/**
65 * Tracks saved or available wifi networks and their state.
66 */
67public class WifiTracker {
68    /**
69     * Default maximum age in millis of cached scored networks in
70     * {@link AccessPoint#mScoredNetworkCache} to be used for speed label generation.
71     */
72    private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS;
73
74    private static final String TAG = "WifiTracker";
75    private static final boolean DBG() {
76        return Log.isLoggable(TAG, Log.DEBUG);
77    }
78
79    /** verbose logging flag. this flag is set thru developer debugging options
80     * and used so as to assist with in-the-field WiFi connectivity debugging  */
81    public static boolean sVerboseLogging;
82
83    // TODO(b/36733768): Remove flag includeSaved and includePasspoints.
84
85    // TODO: Allow control of this?
86    // Combo scans can take 5-6s to complete - set to 10s.
87    private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;
88    private static final int NUM_SCANS_TO_CONFIRM_AP_LOSS = 3;
89
90    private final Context mContext;
91    private final WifiManager mWifiManager;
92    private final IntentFilter mFilter;
93    private final ConnectivityManager mConnectivityManager;
94    private final NetworkRequest mNetworkRequest;
95    private final AtomicBoolean mConnected = new AtomicBoolean(false);
96    private final WifiListener mListener;
97    private final boolean mIncludeSaved;
98    private final boolean mIncludeScans;
99    private final boolean mIncludePasspoints;
100    @VisibleForTesting final MainHandler mMainHandler;
101    @VisibleForTesting final WorkHandler mWorkHandler;
102
103    private WifiTrackerNetworkCallback mNetworkCallback;
104
105    @GuardedBy("mLock")
106    private boolean mRegistered;
107
108    /**
109     * The externally visible access point list.
110     *
111     * Updated using main handler. Clone of this collection is returned from
112     * {@link #getAccessPoints()}
113     */
114    private final List<AccessPoint> mAccessPoints = new ArrayList<>();
115
116    /**
117     * The internal list of access points, synchronized on itself.
118     *
119     * Never exposed outside this class.
120     */
121    @GuardedBy("mLock")
122    private final List<AccessPoint> mInternalAccessPoints = new ArrayList<>();
123
124    /**
125    * Synchronization lock for managing concurrency between main and worker threads.
126    *
127    * <p>This lock should be held for all background work.
128    * TODO(b/37674366): Remove the worker thread so synchronization is no longer necessary.
129    */
130    private final Object mLock = new Object();
131
132    //visible to both worker and main thread.
133    @GuardedBy("mLock")
134    private final AccessPointListenerAdapter mAccessPointListenerAdapter
135            = new AccessPointListenerAdapter();
136
137    private final HashMap<String, Integer> mSeenBssids = new HashMap<>();
138    private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>();
139    private Integer mScanId = 0;
140
141    private NetworkInfo mLastNetworkInfo;
142    private WifiInfo mLastInfo;
143
144    private final NetworkScoreManager mNetworkScoreManager;
145    private final WifiNetworkScoreCache mScoreCache;
146    private boolean mNetworkScoringUiEnabled;
147    private long mMaxSpeedLabelScoreCacheAge;
148
149    @GuardedBy("mLock")
150    private final Set<NetworkKey> mRequestedScores = new ArraySet<>();
151
152    @VisibleForTesting
153    Scanner mScanner;
154
155    @GuardedBy("mLock")
156    private boolean mStaleScanResults = true;
157
158    public WifiTracker(Context context, WifiListener wifiListener,
159            boolean includeSaved, boolean includeScans) {
160        this(context, wifiListener, null, includeSaved, includeScans);
161    }
162
163    public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
164            boolean includeSaved, boolean includeScans) {
165        this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
166    }
167
168    public WifiTracker(Context context, WifiListener wifiListener,
169            boolean includeSaved, boolean includeScans, boolean includePasspoints) {
170        this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
171    }
172
173    public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
174            boolean includeSaved, boolean includeScans, boolean includePasspoints) {
175        this(context, wifiListener, workerLooper, includeSaved, includeScans, includePasspoints,
176                context.getSystemService(WifiManager.class),
177                context.getSystemService(ConnectivityManager.class),
178                context.getSystemService(NetworkScoreManager.class), Looper.myLooper()
179        );
180    }
181
182    @VisibleForTesting
183    WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
184            boolean includeSaved, boolean includeScans, boolean includePasspoints,
185            WifiManager wifiManager, ConnectivityManager connectivityManager,
186            NetworkScoreManager networkScoreManager, Looper currentLooper) {
187        if (!includeSaved && !includeScans) {
188            throw new IllegalArgumentException("Must include either saved or scans");
189        }
190        mContext = context;
191        if (currentLooper == null) {
192            // When we aren't on a looper thread, default to the main.
193            currentLooper = Looper.getMainLooper();
194        }
195        mMainHandler = new MainHandler(currentLooper);
196        mWorkHandler = new WorkHandler(
197                workerLooper != null ? workerLooper : currentLooper);
198        mWifiManager = wifiManager;
199        mIncludeSaved = includeSaved;
200        mIncludeScans = includeScans;
201        mIncludePasspoints = includePasspoints;
202        mListener = wifiListener;
203        mConnectivityManager = connectivityManager;
204
205        // check if verbose logging has been turned on or off
206        sVerboseLogging = (mWifiManager.getVerboseLoggingLevel() > 0);
207
208        mFilter = new IntentFilter();
209        mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
210        mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
211        mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
212        mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
213        mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
214        mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
215        mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
216        mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
217
218        mNetworkRequest = new NetworkRequest.Builder()
219                .clearCapabilities()
220                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
221                .build();
222
223        mNetworkScoreManager = networkScoreManager;
224
225        mScoreCache = new WifiNetworkScoreCache(context, new CacheListener(mWorkHandler) {
226            @Override
227            public void networkCacheUpdated(List<ScoredNetwork> networks) {
228                synchronized (mLock) {
229                    if (!mRegistered) return;
230                }
231
232                if (Log.isLoggable(TAG, Log.VERBOSE)) {
233                    Log.v(TAG, "Score cache was updated with networks: " + networks);
234                }
235                updateNetworkScores();
236            }
237        });
238    }
239
240    /** Synchronously update the list of access points with the latest information. */
241    @MainThread
242    public void forceUpdate() {
243        synchronized (mLock) {
244            mWorkHandler.removeMessages(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
245            mLastInfo = mWifiManager.getConnectionInfo();
246            mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
247
248            final List<ScanResult> newScanResults = mWifiManager.getScanResults();
249            if (sVerboseLogging) {
250                Log.i(TAG, "Fetched scan results: " + newScanResults);
251            }
252
253            List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
254            mInternalAccessPoints.clear();
255            updateAccessPointsLocked(newScanResults, configs);
256
257            // Synchronously copy access points
258            mMainHandler.removeMessages(MainHandler.MSG_ACCESS_POINT_CHANGED);
259            mMainHandler.handleMessage(
260                    Message.obtain(mMainHandler, MainHandler.MSG_ACCESS_POINT_CHANGED));
261            if (sVerboseLogging) {
262                Log.i(TAG, "force update - external access point list:\n" + mAccessPoints);
263            }
264        }
265    }
266
267    /**
268     * Force a scan for wifi networks to happen now.
269     */
270    public void forceScan() {
271        if (mWifiManager.isWifiEnabled() && mScanner != null) {
272            mScanner.forceScan();
273        }
274    }
275
276    /**
277     * Temporarily stop scanning for wifi networks.
278     */
279    public void pauseScanning() {
280        if (mScanner != null) {
281            mScanner.pause();
282            mScanner = null;
283        }
284    }
285
286    /**
287     * Resume scanning for wifi networks after it has been paused.
288     *
289     * <p>The score cache should be registered before this method is invoked.
290     */
291    public void resumeScanning() {
292        if (mScanner == null) {
293            mScanner = new Scanner();
294        }
295
296        mWorkHandler.sendEmptyMessage(WorkHandler.MSG_RESUME);
297        if (mWifiManager.isWifiEnabled()) {
298            mScanner.resume();
299        }
300    }
301
302    /**
303     * Start tracking wifi networks and scores.
304     *
305     * <p>Registers listeners and starts scanning for wifi networks. If this is not called
306     * then forceUpdate() must be called to populate getAccessPoints().
307     */
308    @MainThread
309    public void startTracking() {
310        synchronized (mLock) {
311            registerScoreCache();
312
313            mNetworkScoringUiEnabled =
314                    Settings.Global.getInt(
315                            mContext.getContentResolver(),
316                            Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1;
317
318            mMaxSpeedLabelScoreCacheAge =
319                    Settings.Global.getLong(
320                            mContext.getContentResolver(),
321                            Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS,
322                            DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS);
323
324            resumeScanning();
325            if (!mRegistered) {
326                mContext.registerReceiver(mReceiver, mFilter);
327                // NetworkCallback objects cannot be reused. http://b/20701525 .
328                mNetworkCallback = new WifiTrackerNetworkCallback();
329                mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
330                mRegistered = true;
331            }
332        }
333    }
334
335    private void registerScoreCache() {
336        mNetworkScoreManager.registerNetworkScoreCache(
337                NetworkKey.TYPE_WIFI,
338                mScoreCache,
339                NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
340    }
341
342    private void requestScoresForNetworkKeys(Collection<NetworkKey> keys) {
343        if (keys.isEmpty()) return;
344
345        if (DBG()) {
346            Log.d(TAG, "Requesting scores for Network Keys: " + keys);
347        }
348        mNetworkScoreManager.requestScores(keys.toArray(new NetworkKey[keys.size()]));
349        synchronized (mLock) {
350            mRequestedScores.addAll(keys);
351        }
352    }
353
354    /**
355     * Stop tracking wifi networks and scores.
356     *
357     * <p>This should always be called when done with a WifiTracker (if startTracking was called) to
358     * ensure proper cleanup and prevent any further callbacks from occurring.
359     *
360     * <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents
361     * {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit
362     * is unset on the next SCAN_RESULTS_AVAILABLE_ACTION).
363     */
364    @MainThread
365    public void stopTracking() {
366        synchronized (mLock) {
367            if (mRegistered) {
368                mContext.unregisterReceiver(mReceiver);
369                mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
370                mRegistered = false;
371            }
372            unregisterScoreCache();
373            pauseScanning();
374
375            mWorkHandler.removePendingMessages();
376            mMainHandler.removePendingMessages();
377            mStaleScanResults = true;
378        }
379    }
380
381    private void unregisterScoreCache() {
382        mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mScoreCache);
383
384        // We do not want to clear the existing scores in the cache, as this method is called during
385        // stop tracking on activity pause. Hence, on resumption we want the ability to show the
386        // last known, potentially stale, scores. However, by clearing requested scores, the scores
387        // will be requested again upon resumption of tracking, and if any changes have occurred
388        // the listeners (UI) will be updated accordingly.
389        synchronized (mLock) {
390            mRequestedScores.clear();
391        }
392    }
393
394    /**
395     * Gets the current list of access points. Should be called from main thread, otherwise
396     * expect inconsistencies
397     */
398    @MainThread
399    public List<AccessPoint> getAccessPoints() {
400        return new ArrayList<>(mAccessPoints);
401    }
402
403    public WifiManager getManager() {
404        return mWifiManager;
405    }
406
407    public boolean isWifiEnabled() {
408        return mWifiManager.isWifiEnabled();
409    }
410
411    /**
412     * Returns the number of saved networks on the device, regardless of whether the WifiTracker
413     * is tracking saved networks.
414     * TODO(b/62292448): remove this function and update callsites to use WifiSavedConfigUtils
415     * directly.
416     */
417    public int getNumSavedNetworks() {
418        return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size();
419    }
420
421    public boolean isConnected() {
422        return mConnected.get();
423    }
424
425    public void dump(PrintWriter pw) {
426        pw.println("  - wifi tracker ------");
427        for (AccessPoint accessPoint : getAccessPoints()) {
428            pw.println("  " + accessPoint);
429        }
430    }
431
432    private void handleResume() {
433        mScanResultCache.clear();
434        mSeenBssids.clear();
435        mScanId = 0;
436    }
437
438    private Collection<ScanResult> updateScanResultCache(final List<ScanResult> newResults) {
439        mScanId++;
440        for (ScanResult newResult : newResults) {
441            if (newResult.SSID == null || newResult.SSID.isEmpty()) {
442                continue;
443            }
444            mScanResultCache.put(newResult.BSSID, newResult);
445            mSeenBssids.put(newResult.BSSID, mScanId);
446        }
447
448        if (mScanId > NUM_SCANS_TO_CONFIRM_AP_LOSS) {
449            if (DBG()) Log.d(TAG, "------ Dumping SSIDs that were expired on this scan ------");
450            Integer threshold = mScanId - NUM_SCANS_TO_CONFIRM_AP_LOSS;
451            for (Iterator<Map.Entry<String, Integer>> it = mSeenBssids.entrySet().iterator();
452                    it.hasNext(); /* nothing */) {
453                Map.Entry<String, Integer> e = it.next();
454                if (e.getValue() < threshold) {
455                    ScanResult result = mScanResultCache.get(e.getKey());
456                    if (DBG()) Log.d(TAG, "Removing " + e.getKey() + ":(" + result.SSID + ")");
457                    mScanResultCache.remove(e.getKey());
458                    it.remove();
459                }
460            }
461            if (DBG()) Log.d(TAG, "---- Done Dumping SSIDs that were expired on this scan ----");
462        }
463
464        return mScanResultCache.values();
465    }
466
467    private WifiConfiguration getWifiConfigurationForNetworkId(
468            int networkId, final List<WifiConfiguration> configs) {
469        if (configs != null) {
470            for (WifiConfiguration config : configs) {
471                if (mLastInfo != null && networkId == config.networkId &&
472                        !(config.selfAdded && config.numAssociation == 0)) {
473                    return config;
474                }
475            }
476        }
477        return null;
478    }
479
480    /**
481     * Safely modify {@link #mInternalAccessPoints} by acquiring {@link #mLock} first.
482     *
483     * <p>Will not perform the update if {@link #mStaleScanResults} is true
484     */
485    private void updateAccessPoints() {
486        List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
487        final List<ScanResult> newScanResults = mWifiManager.getScanResults();
488        if (sVerboseLogging) {
489            Log.i(TAG, "Fetched scan results: " + newScanResults);
490        }
491
492        synchronized (mLock) {
493            if(!mStaleScanResults) {
494                updateAccessPointsLocked(newScanResults, configs);
495            }
496        }
497    }
498
499    /**
500     * Update the internal list of access points.
501     *
502     * <p>Do not called directly (except for forceUpdate), use {@link #updateAccessPoints()} which
503     * respects {@link #mStaleScanResults}.
504     */
505    @GuardedBy("mLock")
506    private void updateAccessPointsLocked(final List<ScanResult> newScanResults,
507            List<WifiConfiguration> configs) {
508        WifiConfiguration connectionConfig = null;
509        if (mLastInfo != null) {
510            connectionConfig = getWifiConfigurationForNetworkId(
511                    mLastInfo.getNetworkId(), mWifiManager.getConfiguredNetworks());
512        }
513
514        // Swap the current access points into a cached list.
515        List<AccessPoint> cachedAccessPoints = new ArrayList<>(mInternalAccessPoints);
516        ArrayList<AccessPoint> accessPoints = new ArrayList<>();
517
518        // Clear out the configs so we don't think something is saved when it isn't.
519        for (AccessPoint accessPoint : cachedAccessPoints) {
520            accessPoint.clearConfig();
521        }
522
523    /* Lookup table to more quickly update AccessPoints by only considering objects with the
524     * correct SSID.  Maps SSID -> List of AccessPoints with the given SSID.  */
525        Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>();
526
527        final Collection<ScanResult> results = updateScanResultCache(newScanResults);
528
529        if (configs != null) {
530            for (WifiConfiguration config : configs) {
531                if (config.selfAdded && config.numAssociation == 0) {
532                    continue;
533                }
534                AccessPoint accessPoint = getCachedOrCreate(config, cachedAccessPoints);
535                if (mLastInfo != null && mLastNetworkInfo != null) {
536                    accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
537                }
538                if (mIncludeSaved) {
539                    // If saved network not present in scan result then set its Rssi to
540                    // UNREACHABLE_RSSI
541                    boolean apFound = false;
542                    for (ScanResult result : results) {
543                        if (result.SSID.equals(accessPoint.getSsidStr())) {
544                            apFound = true;
545                            break;
546                        }
547                    }
548                    if (!apFound) {
549                        accessPoint.setUnreachable();
550                    }
551                    accessPoints.add(accessPoint);
552                    apMap.put(accessPoint.getSsidStr(), accessPoint);
553                } else {
554                    // If we aren't using saved networks, drop them into the cache so that
555                    // we have access to their saved info.
556                    cachedAccessPoints.add(accessPoint);
557                }
558            }
559        }
560
561        final List<NetworkKey> scoresToRequest = new ArrayList<>();
562        if (results != null) {
563            for (ScanResult result : results) {
564                // Ignore hidden and ad-hoc networks.
565                if (result.SSID == null || result.SSID.length() == 0 ||
566                        result.capabilities.contains("[IBSS]")) {
567                    continue;
568                }
569
570                NetworkKey key = NetworkKey.createFromScanResult(result);
571                if (key != null && !mRequestedScores.contains(key)) {
572                    scoresToRequest.add(key);
573                }
574
575                boolean found = false;
576                for (AccessPoint accessPoint : apMap.getAll(result.SSID)) {
577                    // We want to evict old scan results if are current results are not stale
578                    if (accessPoint.update(result, !mStaleScanResults)) {
579                        found = true;
580                        break;
581                    }
582                }
583                if (!found && mIncludeScans) {
584                    AccessPoint accessPoint = getCachedOrCreate(result, cachedAccessPoints);
585                    if (mLastInfo != null && mLastNetworkInfo != null) {
586                        accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
587                    }
588
589                    if (result.isPasspointNetwork()) {
590                        // Retrieve a WifiConfiguration for a Passpoint provider that matches
591                        // the given ScanResult.  This is used for showing that a given AP
592                        // (ScanResult) is available via a Passpoint provider (provider friendly
593                        // name).
594                        try {
595                            WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result);
596                            if (config != null) {
597                                accessPoint.update(config);
598                            }
599                        } catch (UnsupportedOperationException e) {
600                            // Passpoint not supported on the device.
601                        }
602                    }
603
604                    accessPoints.add(accessPoint);
605                    apMap.put(accessPoint.getSsidStr(), accessPoint);
606                }
607            }
608        }
609
610        requestScoresForNetworkKeys(scoresToRequest);
611        for (AccessPoint ap : accessPoints) {
612            ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge);
613        }
614
615        // Pre-sort accessPoints to speed preference insertion
616        Collections.sort(accessPoints);
617
618        // Log accesspoints that were deleted
619        if (DBG()) {
620            Log.d(TAG, "------ Dumping SSIDs that were not seen on this scan ------");
621            for (AccessPoint prevAccessPoint : mInternalAccessPoints) {
622                if (prevAccessPoint.getSsid() == null)
623                    continue;
624                String prevSsid = prevAccessPoint.getSsidStr();
625                boolean found = false;
626                for (AccessPoint newAccessPoint : accessPoints) {
627                    if (newAccessPoint.getSsidStr() != null && newAccessPoint.getSsidStr()
628                            .equals(prevSsid)) {
629                        found = true;
630                        break;
631                    }
632                }
633                if (!found)
634                    Log.d(TAG, "Did not find " + prevSsid + " in this scan");
635            }
636            Log.d(TAG, "---- Done dumping SSIDs that were not seen on this scan ----");
637        }
638
639        mInternalAccessPoints.clear();
640        mInternalAccessPoints.addAll(accessPoints);
641
642        mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
643    }
644
645    @VisibleForTesting
646    AccessPoint getCachedOrCreate(ScanResult result, List<AccessPoint> cache) {
647        final int N = cache.size();
648        for (int i = 0; i < N; i++) {
649            if (cache.get(i).matches(result)) {
650                AccessPoint ret = cache.remove(i);
651                // evict old scan results only if we have fresh results
652                ret.update(result, !mStaleScanResults);
653                return ret;
654            }
655        }
656        final AccessPoint accessPoint = new AccessPoint(mContext, result);
657        accessPoint.setListener(mAccessPointListenerAdapter);
658        return accessPoint;
659    }
660
661    @VisibleForTesting
662    AccessPoint getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache) {
663        final int N = cache.size();
664        for (int i = 0; i < N; i++) {
665            if (cache.get(i).matches(config)) {
666                AccessPoint ret = cache.remove(i);
667                ret.loadConfig(config);
668                return ret;
669            }
670        }
671        final AccessPoint accessPoint = new AccessPoint(mContext, config);
672        accessPoint.setListener(mAccessPointListenerAdapter);
673        return accessPoint;
674    }
675
676    private void updateNetworkInfo(NetworkInfo networkInfo) {
677        /* sticky broadcasts can call this when wifi is disabled */
678        if (!mWifiManager.isWifiEnabled()) {
679            clearAccessPointsAndConditionallyUpdate();
680            return;
681        }
682
683        if (networkInfo != null) {
684            mLastNetworkInfo = networkInfo;
685            if (DBG()) {
686                Log.d(TAG, "mLastNetworkInfo set: " + mLastNetworkInfo);
687            }
688        }
689
690        WifiConfiguration connectionConfig = null;
691
692        mLastInfo = mWifiManager.getConnectionInfo();
693        if (DBG()) {
694            Log.d(TAG, "mLastInfo set as: " + mLastInfo);
695        }
696        if (mLastInfo != null) {
697            connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(),
698                    mWifiManager.getConfiguredNetworks());
699        }
700
701        boolean updated = false;
702        boolean reorder = false; // Only reorder if connected AP was changed
703
704        synchronized (mLock) {
705            for (int i = mInternalAccessPoints.size() - 1; i >= 0; --i) {
706                AccessPoint ap = mInternalAccessPoints.get(i);
707                boolean previouslyConnected = ap.isActive();
708                if (ap.update(connectionConfig, mLastInfo, mLastNetworkInfo)) {
709                    updated = true;
710                    if (previouslyConnected != ap.isActive()) reorder = true;
711                }
712                if (ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) {
713                    reorder = true;
714                    updated = true;
715                }
716            }
717
718            if (reorder) Collections.sort(mInternalAccessPoints);
719            if (updated) mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
720        }
721    }
722
723    private void clearAccessPointsAndConditionallyUpdate() {
724        synchronized (mLock) {
725            if (!mInternalAccessPoints.isEmpty()) {
726                mInternalAccessPoints.clear();
727                if (!mMainHandler.hasMessages(MainHandler.MSG_ACCESS_POINT_CHANGED)) {
728                    mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
729                }
730            }
731        }
732    }
733
734    /**
735     * Update all the internal access points rankingScores, badge and metering.
736     *
737     * <p>Will trigger a resort and notify listeners of changes if applicable.
738     *
739     * <p>Synchronized on {@link #mLock}.
740     */
741    private void updateNetworkScores() {
742        synchronized (mLock) {
743            boolean updated = false;
744            for (int i = 0; i < mInternalAccessPoints.size(); i++) {
745                if (mInternalAccessPoints.get(i).update(
746                        mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) {
747                    updated = true;
748                }
749            }
750            if (updated) {
751                Collections.sort(mInternalAccessPoints);
752                mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED);
753            }
754        }
755    }
756
757    private void updateWifiState(int state) {
758        mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_WIFI_STATE, state, 0).sendToTarget();
759        if (!mWifiManager.isWifiEnabled()) {
760            clearAccessPointsAndConditionallyUpdate();
761        }
762    }
763
764    public static List<AccessPoint> getCurrentAccessPoints(Context context, boolean includeSaved,
765            boolean includeScans, boolean includePasspoints) {
766        WifiTracker tracker = new WifiTracker(context,
767                null, null, includeSaved, includeScans, includePasspoints);
768        tracker.forceUpdate();
769        tracker.copyAndNotifyListeners(false /*notifyListeners*/);
770        return tracker.getAccessPoints();
771    }
772
773    @VisibleForTesting
774    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
775        @Override
776        public void onReceive(Context context, Intent intent) {
777            String action = intent.getAction();
778
779            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
780                updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
781                        WifiManager.WIFI_STATE_UNKNOWN));
782            } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
783                mWorkHandler
784                        .obtainMessage(
785                            WorkHandler.MSG_UPDATE_ACCESS_POINTS,
786                            WorkHandler.CLEAR_STALE_SCAN_RESULTS,
787                            0)
788                        .sendToTarget();
789            } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action)
790                    || WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
791                mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
792            } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
793                NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
794
795                if(mConnected.get() != info.isConnected()) {
796                    mConnected.set(info.isConnected());
797                    mMainHandler.sendEmptyMessage(MainHandler.MSG_CONNECTED_CHANGED);
798                }
799
800                mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO, info)
801                        .sendToTarget();
802                mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_ACCESS_POINTS);
803            } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
804                NetworkInfo info =
805                        mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
806                mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO, info)
807                        .sendToTarget();
808            }
809        }
810    };
811
812    private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback {
813        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
814            if (network.equals(mWifiManager.getCurrentNetwork())) {
815                // We don't send a NetworkInfo object along with this message, because even if we
816                // fetch one from ConnectivityManager, it might be older than the most recent
817                // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
818                mWorkHandler.sendEmptyMessage(WorkHandler.MSG_UPDATE_NETWORK_INFO);
819            }
820        }
821    }
822
823    @VisibleForTesting
824    final class MainHandler extends Handler {
825        @VisibleForTesting static final int MSG_CONNECTED_CHANGED = 0;
826        @VisibleForTesting static final int MSG_WIFI_STATE_CHANGED = 1;
827        @VisibleForTesting static final int MSG_ACCESS_POINT_CHANGED = 2;
828        private static final int MSG_RESUME_SCANNING = 3;
829        private static final int MSG_PAUSE_SCANNING = 4;
830
831        public MainHandler(Looper looper) {
832            super(looper);
833        }
834
835        @Override
836        public void handleMessage(Message msg) {
837            if (mListener == null) {
838                return;
839            }
840            switch (msg.what) {
841                case MSG_CONNECTED_CHANGED:
842                    mListener.onConnectedChanged();
843                    break;
844                case MSG_WIFI_STATE_CHANGED:
845                    mListener.onWifiStateChanged(msg.arg1);
846                    break;
847                case MSG_ACCESS_POINT_CHANGED:
848                    // Only notify listeners of changes if we have fresh scan results, otherwise the
849                    // UI will be updated with stale results. We want to copy the APs regardless,
850                    // for instances where forceUpdate was invoked by the caller.
851                    if (mStaleScanResults) {
852                        copyAndNotifyListeners(false /*notifyListeners*/);
853                    } else {
854                        copyAndNotifyListeners(true /*notifyListeners*/);
855                        mListener.onAccessPointsChanged();
856                    }
857                    break;
858                case MSG_RESUME_SCANNING:
859                    if (mScanner != null) {
860                        mScanner.resume();
861                    }
862                    break;
863                case MSG_PAUSE_SCANNING:
864                    if (mScanner != null) {
865                        mScanner.pause();
866                    }
867                    synchronized (mLock) {
868                        mStaleScanResults = true;
869                    }
870                    break;
871            }
872        }
873
874        void removePendingMessages() {
875            removeMessages(MSG_ACCESS_POINT_CHANGED);
876            removeMessages(MSG_CONNECTED_CHANGED);
877            removeMessages(MSG_WIFI_STATE_CHANGED);
878            removeMessages(MSG_PAUSE_SCANNING);
879            removeMessages(MSG_RESUME_SCANNING);
880        }
881    }
882
883    @VisibleForTesting
884    final class WorkHandler extends Handler {
885        private static final int MSG_UPDATE_ACCESS_POINTS = 0;
886        private static final int MSG_UPDATE_NETWORK_INFO = 1;
887        private static final int MSG_RESUME = 2;
888        private static final int MSG_UPDATE_WIFI_STATE = 3;
889
890        private static final int CLEAR_STALE_SCAN_RESULTS = 1;
891
892        public WorkHandler(Looper looper) {
893            super(looper);
894        }
895
896        @Override
897        public void handleMessage(Message msg) {
898            synchronized (mLock) {
899                processMessage(msg);
900            }
901        }
902
903        private void processMessage(Message msg) {
904            if (!mRegistered) return;
905
906            switch (msg.what) {
907                case MSG_UPDATE_ACCESS_POINTS:
908                    if (msg.arg1 == CLEAR_STALE_SCAN_RESULTS) {
909                        mStaleScanResults = false;
910                    }
911                    updateAccessPoints();
912                    break;
913                case MSG_UPDATE_NETWORK_INFO:
914                    updateNetworkInfo((NetworkInfo) msg.obj);
915                    break;
916                case MSG_RESUME:
917                    handleResume();
918                    break;
919                case MSG_UPDATE_WIFI_STATE:
920                    if (msg.arg1 == WifiManager.WIFI_STATE_ENABLED) {
921                        if (mScanner != null) {
922                            // We only need to resume if mScanner isn't null because
923                            // that means we want to be scanning.
924                            mScanner.resume();
925                        }
926                    } else {
927                        mLastInfo = null;
928                        mLastNetworkInfo = null;
929                        if (mScanner != null) {
930                            mScanner.pause();
931                        }
932                        synchronized (mLock) {
933                            mStaleScanResults = true;
934                        }
935                    }
936                    mMainHandler.obtainMessage(MainHandler.MSG_WIFI_STATE_CHANGED, msg.arg1, 0)
937                            .sendToTarget();
938                    break;
939            }
940        }
941
942        private void removePendingMessages() {
943            removeMessages(MSG_UPDATE_ACCESS_POINTS);
944            removeMessages(MSG_UPDATE_NETWORK_INFO);
945            removeMessages(MSG_RESUME);
946            removeMessages(MSG_UPDATE_WIFI_STATE);
947        }
948    }
949
950    @VisibleForTesting
951    class Scanner extends Handler {
952        static final int MSG_SCAN = 0;
953
954        private int mRetry = 0;
955
956        void resume() {
957            if (!hasMessages(MSG_SCAN)) {
958                sendEmptyMessage(MSG_SCAN);
959            }
960        }
961
962        void forceScan() {
963            removeMessages(MSG_SCAN);
964            sendEmptyMessage(MSG_SCAN);
965        }
966
967        void pause() {
968            mRetry = 0;
969            removeMessages(MSG_SCAN);
970        }
971
972        @VisibleForTesting
973        boolean isScanning() {
974            return hasMessages(MSG_SCAN);
975        }
976
977        @Override
978        public void handleMessage(Message message) {
979            if (message.what != MSG_SCAN) return;
980            if (mWifiManager.startScan()) {
981                mRetry = 0;
982            } else if (++mRetry >= 3) {
983                mRetry = 0;
984                if (mContext != null) {
985                    Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
986                }
987                return;
988            }
989            sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS);
990        }
991    }
992
993    /** A restricted multimap for use in constructAccessPoints */
994    private static class Multimap<K,V> {
995        private final HashMap<K,List<V>> store = new HashMap<K,List<V>>();
996        /** retrieve a non-null list of values with key K */
997        List<V> getAll(K key) {
998            List<V> values = store.get(key);
999            return values != null ? values : Collections.<V>emptyList();
1000        }
1001
1002        void put(K key, V val) {
1003            List<V> curVals = store.get(key);
1004            if (curVals == null) {
1005                curVals = new ArrayList<V>(3);
1006                store.put(key, curVals);
1007            }
1008            curVals.add(val);
1009        }
1010    }
1011
1012    public interface WifiListener {
1013        /**
1014         * Called when the state of Wifi has changed, the state will be one of
1015         * the following.
1016         *
1017         * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li>
1018         * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li>
1019         * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li>
1020         * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li>
1021         * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li>
1022         * <p>
1023         *
1024         * @param state The new state of wifi.
1025         */
1026        void onWifiStateChanged(int state);
1027
1028        /**
1029         * Called when the connection state of wifi has changed and isConnected
1030         * should be called to get the updated state.
1031         */
1032        void onConnectedChanged();
1033
1034        /**
1035         * Called to indicate the list of AccessPoints has been updated and
1036         * getAccessPoints should be called to get the latest information.
1037         */
1038        void onAccessPointsChanged();
1039    }
1040
1041    /**
1042     * Helps capture notifications that were generated during AccessPoint modification. Used later
1043     * on by {@link #copyAndNotifyListeners(boolean)} to send notifications.
1044     */
1045    private static class AccessPointListenerAdapter implements AccessPoint.AccessPointListener {
1046        static final int AP_CHANGED = 1;
1047        static final int LEVEL_CHANGED = 2;
1048
1049        final SparseIntArray mPendingNotifications = new SparseIntArray();
1050
1051        @Override
1052        public void onAccessPointChanged(AccessPoint accessPoint) {
1053            int type = mPendingNotifications.get(accessPoint.mId);
1054            mPendingNotifications.put(accessPoint.mId, type | AP_CHANGED);
1055        }
1056
1057        @Override
1058        public void onLevelChanged(AccessPoint accessPoint) {
1059            int type = mPendingNotifications.get(accessPoint.mId);
1060            mPendingNotifications.put(accessPoint.mId, type | LEVEL_CHANGED);
1061        }
1062    }
1063
1064    /**
1065     * Responsible for copying access points from {@link #mInternalAccessPoints} and notifying
1066     * accesspoint listeners.
1067     *
1068     * @param notifyListeners if true, accesspoint listeners are notified, otherwise notifications
1069     *                        dropped.
1070     */
1071    @MainThread
1072    private void copyAndNotifyListeners(boolean notifyListeners) {
1073        // Need to watch out for memory allocations on main thread.
1074        SparseArray<AccessPoint> oldAccessPoints = new SparseArray<>();
1075        SparseIntArray notificationMap = null;
1076        List<AccessPoint> updatedAccessPoints = new ArrayList<>();
1077
1078        for (AccessPoint accessPoint : mAccessPoints) {
1079            oldAccessPoints.put(accessPoint.mId, accessPoint);
1080        }
1081
1082        synchronized (mLock) {
1083            if (DBG()) {
1084                Log.d(TAG, "Starting to copy AP items on the MainHandler. Internal APs: "
1085                        + mInternalAccessPoints);
1086            }
1087
1088            if (notifyListeners) {
1089                notificationMap = mAccessPointListenerAdapter.mPendingNotifications.clone();
1090            }
1091
1092            mAccessPointListenerAdapter.mPendingNotifications.clear();
1093
1094            for (AccessPoint internalAccessPoint : mInternalAccessPoints) {
1095                AccessPoint accessPoint = oldAccessPoints.get(internalAccessPoint.mId);
1096                if (accessPoint == null) {
1097                    accessPoint = new AccessPoint(mContext, internalAccessPoint);
1098                } else {
1099                    accessPoint.copyFrom(internalAccessPoint);
1100                }
1101                updatedAccessPoints.add(accessPoint);
1102            }
1103        }
1104
1105        mAccessPoints.clear();
1106        mAccessPoints.addAll(updatedAccessPoints);
1107
1108        if (notificationMap != null && notificationMap.size() > 0) {
1109            for (AccessPoint accessPoint : updatedAccessPoints) {
1110                int notificationType = notificationMap.get(accessPoint.mId);
1111                AccessPoint.AccessPointListener listener = accessPoint.mAccessPointListener;
1112                if (notificationType == 0 || listener == null) {
1113                    continue;
1114                }
1115
1116                if ((notificationType & AccessPointListenerAdapter.AP_CHANGED) != 0) {
1117                    listener.onAccessPointChanged(accessPoint);
1118                }
1119
1120                if ((notificationType & AccessPointListenerAdapter.LEVEL_CHANGED) != 0) {
1121                    listener.onLevelChanged(accessPoint);
1122                }
1123            }
1124        }
1125    }
1126}
1127