1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settingslib.wifi;
17 
18 import android.annotation.AnyThread;
19 import android.annotation.MainThread;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.net.ConnectivityManager;
25 import android.net.Network;
26 import android.net.NetworkCapabilities;
27 import android.net.NetworkInfo;
28 import android.net.NetworkKey;
29 import android.net.NetworkRequest;
30 import android.net.NetworkScoreManager;
31 import android.net.ScoredNetwork;
32 import android.net.wifi.ScanResult;
33 import android.net.wifi.WifiConfiguration;
34 import android.net.wifi.WifiInfo;
35 import android.net.wifi.WifiManager;
36 import android.net.wifi.WifiNetworkScoreCache;
37 import android.net.wifi.WifiNetworkScoreCache.CacheListener;
38 import android.os.Handler;
39 import android.os.HandlerThread;
40 import android.os.Message;
41 import android.os.Process;
42 import android.os.SystemClock;
43 import android.provider.Settings;
44 import android.support.annotation.GuardedBy;
45 import android.support.annotation.NonNull;
46 import android.support.annotation.VisibleForTesting;
47 import android.text.format.DateUtils;
48 import android.util.ArrayMap;
49 import android.util.ArraySet;
50 import android.util.Log;
51 import android.widget.Toast;
52 
53 import com.android.settingslib.R;
54 import com.android.settingslib.core.lifecycle.Lifecycle;
55 import com.android.settingslib.core.lifecycle.LifecycleObserver;
56 import com.android.settingslib.core.lifecycle.events.OnDestroy;
57 import com.android.settingslib.core.lifecycle.events.OnStart;
58 import com.android.settingslib.core.lifecycle.events.OnStop;
59 import com.android.settingslib.utils.ThreadUtils;
60 
61 import java.io.PrintWriter;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.Collections;
65 import java.util.HashMap;
66 import java.util.Iterator;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Set;
70 import java.util.concurrent.atomic.AtomicBoolean;
71 
72 /**
73  * Tracks saved or available wifi networks and their state.
74  */
75 public 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";
DBG()86     private static final boolean DBG() {
87         return Log.isLoggable(TAG, Log.DEBUG);
88     }
89 
isVerboseLoggingEnabled()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 
newIntentFilter()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
WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans)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
WifiTracker(Context context, WifiListener wifiListener, @NonNull Lifecycle lifecycle, boolean includeSaved, boolean includeScans)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
WifiTracker(Context context, WifiListener wifiListener, WifiManager wifiManager, ConnectivityManager connectivityManager, NetworkScoreManager networkScoreManager, IntentFilter filter)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
setWorkThread(HandlerThread workThread)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
onDestroy()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      */
pauseScanning()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      */
resumeScanning()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
onStart()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
forceUpdate()329     private void forceUpdate() {
330         mLastInfo = mWifiManager.getConnectionInfo();
331         mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
332 
333         fetchScansAndConfigsAndUpdateAccessPoints();
334     }
335 
registerScoreCache()336     private void registerScoreCache() {
337         mNetworkScoreManager.registerNetworkScoreCache(
338                 NetworkKey.TYPE_WIFI,
339                 mScoreCache,
340                 NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
341     }
342 
requestScoresForNetworkKeys(Collection<NetworkKey> keys)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
onStop()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 
unregisterScoreCache()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
getAccessPoints()399     public List<AccessPoint> getAccessPoints() {
400         synchronized (mLock) {
401             return new ArrayList<>(mInternalAccessPoints);
402         }
403     }
404 
getManager()405     public WifiManager getManager() {
406         return mWifiManager;
407     }
408 
isWifiEnabled()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      */
getNumSavedNetworks()419     public int getNumSavedNetworks() {
420         return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size();
421     }
422 
isConnected()423     public boolean isConnected() {
424         return mConnected.get();
425     }
426 
dump(PrintWriter pw)427     public void dump(PrintWriter pw) {
428         pw.println("  - wifi tracker ------");
429         for (AccessPoint accessPoint : getAccessPoints()) {
430             pw.println("  " + accessPoint);
431         }
432     }
433 
updateScanResultCache( final List<ScanResult> newResults)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      */
evictOldScans()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 
getWifiConfigurationForNetworkId( int networkId, final List<WifiConfiguration> configs)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      */
fetchScansAndConfigsAndUpdateAccessPoints()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. */
updateAccessPoints(final List<ScanResult> newScanResults, List<WifiConfiguration> configs)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
getCachedOrCreate( List<ScanResult> scanResults, List<AccessPoint> cache)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 
updateNetworkInfo(NetworkInfo networkInfo)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      */
clearAccessPointsAndConditionallyUpdate()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      */
updateNetworkScores()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      */
updateWifiState(int state)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 {
onCapabilitiesChanged(Network network, NetworkCapabilities nc)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 
resume()803         void resume() {
804             if (!hasMessages(MSG_SCAN)) {
805                 sendEmptyMessage(MSG_SCAN);
806             }
807         }
808 
pause()809         void pause() {
810             mRetry = 0;
811             removeMessages(MSG_SCAN);
812         }
813 
814         @VisibleForTesting
isScanning()815         boolean isScanning() {
816             return hasMessages(MSG_SCAN);
817         }
818 
819         @Override
handleMessage(Message message)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 */
getAll(K key)839         List<V> getAll(K key) {
840             List<V> values = store.get(key);
841             return values != null ? values : Collections.<V>emptyList();
842         }
843 
put(K key, V val)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 
WifiListenerExecutor(WifiListener listener)863         public WifiListenerExecutor(WifiListener listener) {
864             mDelegatee = listener;
865         }
866 
867         @Override
onWifiStateChanged(int state)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
onConnectedChanged()874         public void onConnectedChanged() {
875             runAndLog(mDelegatee::onConnectedChanged, "Invoking onConnectedChanged callback");
876         }
877 
878         @Override
onAccessPointsChanged()879         public void onAccessPointsChanged() {
880             runAndLog(mDelegatee::onAccessPointsChanged, "Invoking onAccessPointsChanged callback");
881         }
882 
runAndLog(Runnable r, String verboseLog)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          */
onWifiStateChanged(int state)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          */
onConnectedChanged()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          */
onAccessPointsChanged()926         void onAccessPointsChanged();
927     }
928 
929     /**
930      * Invokes {@link WifiListenerExecutor#onAccessPointsChanged()} iif {@link #mStaleScanResults}
931      * is false.
932      */
conditionallyNotifyListeners()933     private void conditionallyNotifyListeners() {
934         if (mStaleScanResults) {
935             return;
936         }
937 
938         mListener.onAccessPointsChanged();
939     }
940 }
941