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.AnyThread;
19import android.annotation.MainThread;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.net.ConnectivityManager;
25import android.net.Network;
26import android.net.NetworkCapabilities;
27import android.net.NetworkInfo;
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.HandlerThread;
40import android.os.Message;
41import android.os.Process;
42import android.os.SystemClock;
43import android.provider.Settings;
44import android.support.annotation.GuardedBy;
45import android.support.annotation.NonNull;
46import android.support.annotation.VisibleForTesting;
47import android.text.format.DateUtils;
48import android.util.ArrayMap;
49import android.util.ArraySet;
50import android.util.Log;
51import android.widget.Toast;
52
53import com.android.settingslib.R;
54import com.android.settingslib.core.lifecycle.Lifecycle;
55import com.android.settingslib.core.lifecycle.LifecycleObserver;
56import com.android.settingslib.core.lifecycle.events.OnDestroy;
57import com.android.settingslib.core.lifecycle.events.OnStart;
58import com.android.settingslib.core.lifecycle.events.OnStop;
59import com.android.settingslib.utils.ThreadUtils;
60
61import java.io.PrintWriter;
62import java.util.ArrayList;
63import java.util.Collection;
64import java.util.Collections;
65import java.util.HashMap;
66import java.util.Iterator;
67import java.util.List;
68import java.util.Map;
69import java.util.Set;
70import java.util.concurrent.atomic.AtomicBoolean;
71
72/**
73 * Tracks saved or available wifi networks and their state.
74 */
75public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy {
76    /**
77     * Default maximum age in millis of cached scored networks in
78     * {@link AccessPoint#mScoredNetworkCache} to be used for speed label generation.
79     */
80    private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS;
81
82    /** Maximum age of scan results to hold onto while actively scanning. **/
83    private static final long MAX_SCAN_RESULT_AGE_MILLIS = 25000;
84
85    private static final String TAG = "WifiTracker";
86    private static final boolean DBG() {
87        return Log.isLoggable(TAG, Log.DEBUG);
88    }
89
90    private static boolean isVerboseLoggingEnabled() {
91        return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE);
92    }
93
94    /**
95     * Verbose logging flag set thru developer debugging options and used so as to assist with
96     * in-the-field WiFi connectivity debugging.
97     *
98     * <p>{@link #isVerboseLoggingEnabled()} should be read rather than referencing this value
99     * directly, to ensure adb TAG level verbose settings are respected.
100     */
101    public static boolean sVerboseLogging;
102
103    // TODO: Allow control of this?
104    // Combo scans can take 5-6s to complete - set to 10s.
105    private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;
106
107    private final Context mContext;
108    private final WifiManager mWifiManager;
109    private final IntentFilter mFilter;
110    private final ConnectivityManager mConnectivityManager;
111    private final NetworkRequest mNetworkRequest;
112    private final AtomicBoolean mConnected = new AtomicBoolean(false);
113    private final WifiListenerExecutor mListener;
114    @VisibleForTesting Handler mWorkHandler;
115    private HandlerThread mWorkThread;
116
117    private WifiTrackerNetworkCallback mNetworkCallback;
118
119    /**
120     * Synchronization lock for managing concurrency between main and worker threads.
121     *
122     * <p>This lock should be held for all modifications to {@link #mInternalAccessPoints}.
123     */
124    private final Object mLock = new Object();
125
126    /** The list of AccessPoints, aggregated visible ScanResults with metadata. */
127    @GuardedBy("mLock")
128    private final List<AccessPoint> mInternalAccessPoints = new ArrayList<>();
129
130    @GuardedBy("mLock")
131    private final Set<NetworkKey> mRequestedScores = new ArraySet<>();
132
133    /**
134     * Tracks whether fresh scan results have been received since scanning start.
135     *
136     * <p>If this variable is false, we will not evict the scan result cache or invoke callbacks
137     * so that we do not update the UI with stale data / clear out existing UI elements prematurely.
138     */
139    private boolean mStaleScanResults = true;
140
141    // Does not need to be locked as it only updated on the worker thread, with the exception of
142    // during onStart, which occurs before the receiver is registered on the work handler.
143    private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>();
144    private boolean mRegistered;
145
146    private NetworkInfo mLastNetworkInfo;
147    private WifiInfo mLastInfo;
148
149    private final NetworkScoreManager mNetworkScoreManager;
150    private WifiNetworkScoreCache mScoreCache;
151    private boolean mNetworkScoringUiEnabled;
152    private long mMaxSpeedLabelScoreCacheAge;
153
154
155
156    @VisibleForTesting
157    Scanner mScanner;
158
159    private static IntentFilter newIntentFilter() {
160        IntentFilter filter = new IntentFilter();
161        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
162        filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
163        filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
164        filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
165        filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
166        filter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
167        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
168        filter.addAction(WifiManager.RSSI_CHANGED_ACTION);
169
170        return filter;
171    }
172
173    /**
174     * Use the lifecycle constructor below whenever possible
175     */
176    @Deprecated
177    public WifiTracker(Context context, WifiListener wifiListener,
178            boolean includeSaved, boolean includeScans) {
179        this(context, wifiListener,
180                context.getSystemService(WifiManager.class),
181                context.getSystemService(ConnectivityManager.class),
182                context.getSystemService(NetworkScoreManager.class),
183                newIntentFilter());
184    }
185
186    // TODO(sghuman): Clean up includeSaved and includeScans from all constructors and linked
187    // calling apps once IC window is complete
188    public WifiTracker(Context context, WifiListener wifiListener,
189            @NonNull Lifecycle lifecycle, boolean includeSaved, boolean includeScans) {
190        this(context, wifiListener,
191                context.getSystemService(WifiManager.class),
192                context.getSystemService(ConnectivityManager.class),
193                context.getSystemService(NetworkScoreManager.class),
194                newIntentFilter());
195
196        lifecycle.addObserver(this);
197    }
198
199    @VisibleForTesting
200    WifiTracker(Context context, WifiListener wifiListener,
201            WifiManager wifiManager, ConnectivityManager connectivityManager,
202            NetworkScoreManager networkScoreManager,
203            IntentFilter filter) {
204        mContext = context;
205        mWifiManager = wifiManager;
206        mListener = new WifiListenerExecutor(wifiListener);
207        mConnectivityManager = connectivityManager;
208
209        // check if verbose logging developer option has been turned on or off
210        sVerboseLogging = (mWifiManager.getVerboseLoggingLevel() > 0);
211
212        mFilter = filter;
213
214        mNetworkRequest = new NetworkRequest.Builder()
215                .clearCapabilities()
216                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
217                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
218                .build();
219
220        mNetworkScoreManager = networkScoreManager;
221
222        // TODO(sghuman): Remove this and create less hacky solution for testing
223        final HandlerThread workThread = new HandlerThread(TAG
224                + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
225                Process.THREAD_PRIORITY_BACKGROUND);
226        workThread.start();
227        setWorkThread(workThread);
228    }
229
230    /**
231     * Sanity warning: this wipes out mScoreCache, so use with extreme caution
232     * @param workThread substitute Handler thread, for testing purposes only
233     */
234    @VisibleForTesting
235    // TODO(sghuman): Remove this method, this needs to happen in a factory method and be passed in
236    // during construction
237    void setWorkThread(HandlerThread workThread) {
238        mWorkThread = workThread;
239        mWorkHandler = new Handler(workThread.getLooper());
240        mScoreCache = new WifiNetworkScoreCache(mContext, new CacheListener(mWorkHandler) {
241            @Override
242            public void networkCacheUpdated(List<ScoredNetwork> networks) {
243                if (!mRegistered) return;
244
245                if (Log.isLoggable(TAG, Log.VERBOSE)) {
246                    Log.v(TAG, "Score cache was updated with networks: " + networks);
247                }
248                updateNetworkScores();
249            }
250        });
251    }
252
253    @Override
254    public void onDestroy() {
255        mWorkThread.quit();
256    }
257
258    /**
259     * Temporarily stop scanning for wifi networks.
260     *
261     * <p>Sets {@link #mStaleScanResults} to true.
262     */
263    private void pauseScanning() {
264        if (mScanner != null) {
265            mScanner.pause();
266            mScanner = null;
267        }
268        mStaleScanResults = true;
269    }
270
271    /**
272     * Resume scanning for wifi networks after it has been paused.
273     *
274     * <p>The score cache should be registered before this method is invoked.
275     */
276    public void resumeScanning() {
277        if (mScanner == null) {
278            mScanner = new Scanner();
279        }
280
281        if (mWifiManager.isWifiEnabled()) {
282            mScanner.resume();
283        }
284    }
285
286    /**
287     * Start tracking wifi networks and scores.
288     *
289     * <p>Registers listeners and starts scanning for wifi networks. If this is not called
290     * then forceUpdate() must be called to populate getAccessPoints().
291     */
292    @Override
293    @MainThread
294    public void onStart() {
295        // fetch current ScanResults instead of waiting for broadcast of fresh results
296        forceUpdate();
297
298        registerScoreCache();
299
300        mNetworkScoringUiEnabled =
301                Settings.Global.getInt(
302                        mContext.getContentResolver(),
303                        Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1;
304
305        mMaxSpeedLabelScoreCacheAge =
306                Settings.Global.getLong(
307                        mContext.getContentResolver(),
308                        Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS,
309                        DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS);
310
311        resumeScanning();
312        if (!mRegistered) {
313            mContext.registerReceiver(mReceiver, mFilter, null /* permission */, mWorkHandler);
314            // NetworkCallback objects cannot be reused. http://b/20701525 .
315            mNetworkCallback = new WifiTrackerNetworkCallback();
316            mConnectivityManager.registerNetworkCallback(
317                    mNetworkRequest, mNetworkCallback, mWorkHandler);
318            mRegistered = true;
319        }
320    }
321
322
323    /**
324     * Synchronously update the list of access points with the latest information.
325     *
326     * <p>Intended to only be invoked within {@link #onStart()}.
327     */
328    @MainThread
329    private void forceUpdate() {
330        mLastInfo = mWifiManager.getConnectionInfo();
331        mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
332
333        fetchScansAndConfigsAndUpdateAccessPoints();
334    }
335
336    private void registerScoreCache() {
337        mNetworkScoreManager.registerNetworkScoreCache(
338                NetworkKey.TYPE_WIFI,
339                mScoreCache,
340                NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
341    }
342
343    private void requestScoresForNetworkKeys(Collection<NetworkKey> keys) {
344        if (keys.isEmpty()) return;
345
346        if (DBG()) {
347            Log.d(TAG, "Requesting scores for Network Keys: " + keys);
348        }
349        mNetworkScoreManager.requestScores(keys.toArray(new NetworkKey[keys.size()]));
350        synchronized (mLock) {
351            mRequestedScores.addAll(keys);
352        }
353    }
354
355    /**
356     * Stop tracking wifi networks and scores.
357     *
358     * <p>This should always be called when done with a WifiTracker (if onStart was called) to
359     * ensure proper cleanup and prevent any further callbacks from occurring.
360     *
361     * <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents
362     * {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit
363     * is unset on the next SCAN_RESULTS_AVAILABLE_ACTION).
364     */
365    @Override
366    @MainThread
367    public void onStop() {
368        if (mRegistered) {
369            mContext.unregisterReceiver(mReceiver);
370            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
371            mRegistered = false;
372        }
373        unregisterScoreCache();
374        pauseScanning(); // and set mStaleScanResults
375
376        mWorkHandler.removeCallbacksAndMessages(null /* remove all */);
377    }
378
379    private void unregisterScoreCache() {
380        mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mScoreCache);
381
382        // We do not want to clear the existing scores in the cache, as this method is called during
383        // stop tracking on activity pause. Hence, on resumption we want the ability to show the
384        // last known, potentially stale, scores. However, by clearing requested scores, the scores
385        // will be requested again upon resumption of tracking, and if any changes have occurred
386        // the listeners (UI) will be updated accordingly.
387        synchronized (mLock) {
388            mRequestedScores.clear();
389        }
390    }
391
392    /**
393     * Gets the current list of access points.
394     *
395     * <p>This method is can be called on an abitrary thread by clients, but is normally called on
396     * the UI Thread by the rendering App.
397     */
398    @AnyThread
399    public List<AccessPoint> getAccessPoints() {
400        synchronized (mLock) {
401            return new ArrayList<>(mInternalAccessPoints);
402        }
403    }
404
405    public WifiManager getManager() {
406        return mWifiManager;
407    }
408
409    public boolean isWifiEnabled() {
410        return mWifiManager.isWifiEnabled();
411    }
412
413    /**
414     * Returns the number of saved networks on the device, regardless of whether the WifiTracker
415     * is tracking saved networks.
416     * TODO(b/62292448): remove this function and update callsites to use WifiSavedConfigUtils
417     * directly.
418     */
419    public int getNumSavedNetworks() {
420        return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size();
421    }
422
423    public boolean isConnected() {
424        return mConnected.get();
425    }
426
427    public void dump(PrintWriter pw) {
428        pw.println("  - wifi tracker ------");
429        for (AccessPoint accessPoint : getAccessPoints()) {
430            pw.println("  " + accessPoint);
431        }
432    }
433
434    private ArrayMap<String, List<ScanResult>> updateScanResultCache(
435            final List<ScanResult> newResults) {
436        // TODO(sghuman): Delete this and replace it with the Map of Ap Keys to ScanResults for
437        // memory efficiency
438        for (ScanResult newResult : newResults) {
439            if (newResult.SSID == null || newResult.SSID.isEmpty()) {
440                continue;
441            }
442            mScanResultCache.put(newResult.BSSID, newResult);
443        }
444
445        // Don't evict old results if no new scan results
446        if (!mStaleScanResults) {
447            evictOldScans();
448        }
449
450        ArrayMap<String, List<ScanResult>> scanResultsByApKey = new ArrayMap<>();
451        for (ScanResult result : mScanResultCache.values()) {
452            // Ignore hidden and ad-hoc networks.
453            if (result.SSID == null || result.SSID.length() == 0 ||
454                    result.capabilities.contains("[IBSS]")) {
455                continue;
456            }
457
458            String apKey = AccessPoint.getKey(result);
459            List<ScanResult> resultList;
460            if (scanResultsByApKey.containsKey(apKey)) {
461                resultList = scanResultsByApKey.get(apKey);
462            } else {
463                resultList = new ArrayList<>();
464                scanResultsByApKey.put(apKey, resultList);
465            }
466
467            resultList.add(result);
468        }
469
470        return scanResultsByApKey;
471    }
472
473    /**
474     * Remove old scan results from the cache.
475     *
476     * <p>Should only ever be invoked from {@link #updateScanResultCache(List)} when
477     * {@link #mStaleScanResults} is false.
478     */
479    private void evictOldScans() {
480        long nowMs = SystemClock.elapsedRealtime();
481        for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
482            ScanResult result = iter.next();
483            // result timestamp is in microseconds
484            if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MILLIS) {
485                iter.remove();
486            }
487        }
488    }
489
490    private WifiConfiguration getWifiConfigurationForNetworkId(
491            int networkId, final List<WifiConfiguration> configs) {
492        if (configs != null) {
493            for (WifiConfiguration config : configs) {
494                if (mLastInfo != null && networkId == config.networkId &&
495                        !(config.selfAdded && config.numAssociation == 0)) {
496                    return config;
497                }
498            }
499        }
500        return null;
501    }
502
503    /**
504     * Retrieves latest scan results and wifi configs, then calls
505     * {@link #updateAccessPoints(List, List)}.
506     */
507    private void fetchScansAndConfigsAndUpdateAccessPoints() {
508        final List<ScanResult> newScanResults = mWifiManager.getScanResults();
509        if (isVerboseLoggingEnabled()) {
510            Log.i(TAG, "Fetched scan results: " + newScanResults);
511        }
512
513        List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
514        updateAccessPoints(newScanResults, configs);
515    }
516
517    /** Update the internal list of access points. */
518    private void updateAccessPoints(final List<ScanResult> newScanResults,
519            List<WifiConfiguration> configs) {
520
521        // Map configs and scan results necessary to make AccessPoints
522        final Map<String, WifiConfiguration> configsByKey = new ArrayMap(configs.size());
523        if (configs != null) {
524            for (WifiConfiguration config : configs) {
525                configsByKey.put(AccessPoint.getKey(config), config);
526            }
527        }
528        ArrayMap<String, List<ScanResult>> scanResultsByApKey =
529                updateScanResultCache(newScanResults);
530
531        WifiConfiguration connectionConfig = null;
532        if (mLastInfo != null) {
533            connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(), configs);
534        }
535
536        // Rather than dropping and reacquiring the lock multiple times in this method, we lock
537        // once for efficiency of lock acquisition time and readability
538        synchronized (mLock) {
539            // Swap the current access points into a cached list for maintaining AP listeners
540            List<AccessPoint> cachedAccessPoints;
541            cachedAccessPoints = new ArrayList<>(mInternalAccessPoints);
542
543            ArrayList<AccessPoint> accessPoints = new ArrayList<>();
544
545            final List<NetworkKey> scoresToRequest = new ArrayList<>();
546
547            for (Map.Entry<String, List<ScanResult>> entry : scanResultsByApKey.entrySet()) {
548                for (ScanResult result : entry.getValue()) {
549                    NetworkKey key = NetworkKey.createFromScanResult(result);
550                    if (key != null && !mRequestedScores.contains(key)) {
551                        scoresToRequest.add(key);
552                    }
553                }
554
555                AccessPoint accessPoint =
556                        getCachedOrCreate(entry.getValue(), cachedAccessPoints);
557                if (mLastInfo != null && mLastNetworkInfo != null) {
558                    accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
559                }
560
561                // Update the matching config if there is one, to populate saved network info
562                accessPoint.update(configsByKey.get(entry.getKey()));
563
564                accessPoints.add(accessPoint);
565            }
566
567            // If there were no scan results, create an AP for the currently connected network (if
568            // it exists).
569            // TODO(b/b/73076869): Add support for passpoint (ephemeral) networks
570            if (accessPoints.isEmpty() && connectionConfig != null) {
571                AccessPoint activeAp = new AccessPoint(mContext, connectionConfig);
572                activeAp.update(connectionConfig, mLastInfo, mLastNetworkInfo);
573                accessPoints.add(activeAp);
574                scoresToRequest.add(NetworkKey.createFromWifiInfo(mLastInfo));
575            }
576
577            requestScoresForNetworkKeys(scoresToRequest);
578            for (AccessPoint ap : accessPoints) {
579                ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge);
580            }
581
582            // Pre-sort accessPoints to speed preference insertion
583            Collections.sort(accessPoints);
584
585            // Log accesspoints that are being removed
586            if (DBG()) {
587                Log.d(TAG, "------ Dumping SSIDs that were not seen on this scan ------");
588                for (AccessPoint prevAccessPoint : mInternalAccessPoints) {
589                    if (prevAccessPoint.getSsid() == null)
590                        continue;
591                    String prevSsid = prevAccessPoint.getSsidStr();
592                    boolean found = false;
593                    for (AccessPoint newAccessPoint : accessPoints) {
594                        if (newAccessPoint.getSsidStr() != null && newAccessPoint.getSsidStr()
595                                .equals(prevSsid)) {
596                            found = true;
597                            break;
598                        }
599                    }
600                    if (!found)
601                        Log.d(TAG, "Did not find " + prevSsid + " in this scan");
602                }
603                Log.d(TAG, "---- Done dumping SSIDs that were not seen on this scan ----");
604            }
605
606            mInternalAccessPoints.clear();
607            mInternalAccessPoints.addAll(accessPoints);
608        }
609
610        conditionallyNotifyListeners();
611    }
612
613    @VisibleForTesting
614    AccessPoint getCachedOrCreate(
615            List<ScanResult> scanResults,
616            List<AccessPoint> cache) {
617        final int N = cache.size();
618        for (int i = 0; i < N; i++) {
619            if (cache.get(i).getKey().equals(AccessPoint.getKey(scanResults.get(0)))) {
620                AccessPoint ret = cache.remove(i);
621                ret.setScanResults(scanResults);
622                return ret;
623            }
624        }
625        final AccessPoint accessPoint = new AccessPoint(mContext, scanResults);
626        return accessPoint;
627    }
628
629    private void updateNetworkInfo(NetworkInfo networkInfo) {
630
631        /* Sticky broadcasts can call this when wifi is disabled */
632        if (!mWifiManager.isWifiEnabled()) {
633            clearAccessPointsAndConditionallyUpdate();
634            return;
635        }
636
637        if (networkInfo != null) {
638            mLastNetworkInfo = networkInfo;
639            if (DBG()) {
640                Log.d(TAG, "mLastNetworkInfo set: " + mLastNetworkInfo);
641            }
642
643            if(networkInfo.isConnected() != mConnected.getAndSet(networkInfo.isConnected())) {
644                mListener.onConnectedChanged();
645            }
646        }
647
648        WifiConfiguration connectionConfig = null;
649
650        mLastInfo = mWifiManager.getConnectionInfo();
651        if (DBG()) {
652            Log.d(TAG, "mLastInfo set as: " + mLastInfo);
653        }
654        if (mLastInfo != null) {
655            connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(),
656                    mWifiManager.getConfiguredNetworks());
657        }
658
659        boolean updated = false;
660        boolean reorder = false; // Only reorder if connected AP was changed
661
662        synchronized (mLock) {
663            for (int i = mInternalAccessPoints.size() - 1; i >= 0; --i) {
664                AccessPoint ap = mInternalAccessPoints.get(i);
665                boolean previouslyConnected = ap.isActive();
666                if (ap.update(connectionConfig, mLastInfo, mLastNetworkInfo)) {
667                    updated = true;
668                    if (previouslyConnected != ap.isActive()) reorder = true;
669                }
670                if (ap.update(mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) {
671                    reorder = true;
672                    updated = true;
673                }
674            }
675
676            if (reorder) {
677                Collections.sort(mInternalAccessPoints);
678            }
679            if (updated) {
680                conditionallyNotifyListeners();
681            }
682        }
683    }
684
685    /**
686     * Clears the access point list and conditionally invokes
687     * {@link WifiListener#onAccessPointsChanged()} if required (i.e. the list was not already
688     * empty).
689     */
690    private void clearAccessPointsAndConditionallyUpdate() {
691        synchronized (mLock) {
692            if (!mInternalAccessPoints.isEmpty()) {
693                mInternalAccessPoints.clear();
694                conditionallyNotifyListeners();
695            }
696        }
697    }
698
699    /**
700     * Update all the internal access points rankingScores, badge and metering.
701     *
702     * <p>Will trigger a resort and notify listeners of changes if applicable.
703     *
704     * <p>Synchronized on {@link #mLock}.
705     */
706    private void updateNetworkScores() {
707        synchronized (mLock) {
708            boolean updated = false;
709            for (int i = 0; i < mInternalAccessPoints.size(); i++) {
710                if (mInternalAccessPoints.get(i).update(
711                        mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) {
712                    updated = true;
713                }
714            }
715            if (updated) {
716                Collections.sort(mInternalAccessPoints);
717                conditionallyNotifyListeners();
718            }
719        }
720    }
721
722    /**
723     *  Receiver for handling broadcasts.
724     *
725     *  This receiver is registered on the WorkHandler.
726     */
727    @VisibleForTesting
728    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
729        @Override
730        public void onReceive(Context context, Intent intent) {
731            String action = intent.getAction();
732
733            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
734                updateWifiState(
735                        intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
736                                WifiManager.WIFI_STATE_UNKNOWN));
737            } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
738                mStaleScanResults = false;
739
740                fetchScansAndConfigsAndUpdateAccessPoints();
741            } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action)
742                    || WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
743                fetchScansAndConfigsAndUpdateAccessPoints();
744            } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
745                // TODO(sghuman): Refactor these methods so they cannot result in duplicate
746                // onAccessPointsChanged updates being called from this intent.
747                NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
748                updateNetworkInfo(info);
749                fetchScansAndConfigsAndUpdateAccessPoints();
750            } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
751                NetworkInfo info =
752                        mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
753                updateNetworkInfo(info);
754            }
755        }
756    };
757
758    /**
759     * Handles updates to WifiState.
760     *
761     * <p>If Wifi is not enabled in the enabled state, {@link #mStaleScanResults} will be set to
762     * true.
763     */
764    private void updateWifiState(int state) {
765        if (state == WifiManager.WIFI_STATE_ENABLED) {
766            if (mScanner != null) {
767                // We only need to resume if mScanner isn't null because
768                // that means we want to be scanning.
769                mScanner.resume();
770            }
771        } else {
772            clearAccessPointsAndConditionallyUpdate();
773            mLastInfo = null;
774            mLastNetworkInfo = null;
775            if (mScanner != null) {
776                mScanner.pause();
777            }
778            mStaleScanResults = true;
779        }
780        mListener.onWifiStateChanged(state);
781    }
782
783    private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback {
784        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
785            if (network.equals(mWifiManager.getCurrentNetwork())) {
786                // TODO(sghuman): Investigate whether this comment still holds true and if it makes
787                // more sense fetch the latest network info here:
788
789                // We don't send a NetworkInfo object along with this message, because even if we
790                // fetch one from ConnectivityManager, it might be older than the most recent
791                // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
792                updateNetworkInfo(null);
793            }
794        }
795    }
796
797    @VisibleForTesting
798    class Scanner extends Handler {
799        static final int MSG_SCAN = 0;
800
801        private int mRetry = 0;
802
803        void resume() {
804            if (!hasMessages(MSG_SCAN)) {
805                sendEmptyMessage(MSG_SCAN);
806            }
807        }
808
809        void pause() {
810            mRetry = 0;
811            removeMessages(MSG_SCAN);
812        }
813
814        @VisibleForTesting
815        boolean isScanning() {
816            return hasMessages(MSG_SCAN);
817        }
818
819        @Override
820        public void handleMessage(Message message) {
821            if (message.what != MSG_SCAN) return;
822            if (mWifiManager.startScan()) {
823                mRetry = 0;
824            } else if (++mRetry >= 3) {
825                mRetry = 0;
826                if (mContext != null) {
827                    Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
828                }
829                return;
830            }
831            sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS);
832        }
833    }
834
835    /** A restricted multimap for use in constructAccessPoints */
836    private static class Multimap<K,V> {
837        private final HashMap<K,List<V>> store = new HashMap<K,List<V>>();
838        /** retrieve a non-null list of values with key K */
839        List<V> getAll(K key) {
840            List<V> values = store.get(key);
841            return values != null ? values : Collections.<V>emptyList();
842        }
843
844        void put(K key, V val) {
845            List<V> curVals = store.get(key);
846            if (curVals == null) {
847                curVals = new ArrayList<V>(3);
848                store.put(key, curVals);
849            }
850            curVals.add(val);
851        }
852    }
853
854    /**
855     * Wraps the given {@link WifiListener} instance and executes its methods on the Main Thread.
856     *
857     * <p>Also logs all callbacks invocations when verbose logging is enabled.
858     */
859    @VisibleForTesting class WifiListenerExecutor implements WifiListener {
860
861        private final WifiListener mDelegatee;
862
863        public WifiListenerExecutor(WifiListener listener) {
864            mDelegatee = listener;
865        }
866
867        @Override
868        public void onWifiStateChanged(int state) {
869            runAndLog(() -> mDelegatee.onWifiStateChanged(state),
870                    String.format("Invoking onWifiStateChanged callback with state %d", state));
871        }
872
873        @Override
874        public void onConnectedChanged() {
875            runAndLog(mDelegatee::onConnectedChanged, "Invoking onConnectedChanged callback");
876        }
877
878        @Override
879        public void onAccessPointsChanged() {
880            runAndLog(mDelegatee::onAccessPointsChanged, "Invoking onAccessPointsChanged callback");
881        }
882
883        private void runAndLog(Runnable r, String verboseLog) {
884            ThreadUtils.postOnMainThread(() -> {
885                if (mRegistered) {
886                    if (isVerboseLoggingEnabled()) {
887                        Log.i(TAG, verboseLog);
888                    }
889                    r.run();
890                }
891            });
892        }
893    }
894
895    /**
896     * WifiListener interface that defines callbacks indicating state changes in WifiTracker.
897     *
898     * <p>All callbacks are invoked on the MainThread.
899     */
900    public interface WifiListener {
901        /**
902         * Called when the state of Wifi has changed, the state will be one of
903         * the following.
904         *
905         * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li>
906         * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li>
907         * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li>
908         * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li>
909         * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li>
910         * <p>
911         *
912         * @param state The new state of wifi.
913         */
914        void onWifiStateChanged(int state);
915
916        /**
917         * Called when the connection state of wifi has changed and
918         * {@link WifiTracker#isConnected()} should be called to get the updated state.
919         */
920        void onConnectedChanged();
921
922        /**
923         * Called to indicate the list of AccessPoints has been updated and
924         * {@link WifiTracker#getAccessPoints()} should be called to get the updated list.
925         */
926        void onAccessPointsChanged();
927    }
928
929    /**
930     * Invokes {@link WifiListenerExecutor#onAccessPointsChanged()} iif {@link #mStaleScanResults}
931     * is false.
932     */
933    private void conditionallyNotifyListeners() {
934        if (mStaleScanResults) {
935            return;
936        }
937
938        mListener.onAccessPointsChanged();
939    }
940}
941