• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.MainThread;
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.net.ConnectivityManager;
24 import android.net.Network;
25 import android.net.NetworkCapabilities;
26 import android.net.NetworkInfo;
27 import android.net.NetworkInfo.DetailedState;
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.Looper;
40 import android.os.Message;
41 import android.provider.Settings;
42 import android.support.annotation.GuardedBy;
43 import android.text.format.DateUtils;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.util.SparseArray;
47 import android.util.SparseIntArray;
48 import android.widget.Toast;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.settingslib.R;
52 
53 import java.io.PrintWriter;
54 import java.util.ArrayList;
55 import java.util.Collection;
56 import java.util.Collections;
57 import java.util.HashMap;
58 import java.util.Iterator;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.concurrent.atomic.AtomicBoolean;
63 
64 /**
65  * Tracks saved or available wifi networks and their state.
66  */
67 public 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";
DBG()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 
WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans)158     public WifiTracker(Context context, WifiListener wifiListener,
159             boolean includeSaved, boolean includeScans) {
160         this(context, wifiListener, null, includeSaved, includeScans);
161     }
162 
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans)163     public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
164             boolean includeSaved, boolean includeScans) {
165         this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
166     }
167 
WifiTracker(Context context, WifiListener wifiListener, boolean includeSaved, boolean includeScans, boolean includePasspoints)168     public WifiTracker(Context context, WifiListener wifiListener,
169             boolean includeSaved, boolean includeScans, boolean includePasspoints) {
170         this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
171     }
172 
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints)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
WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper, boolean includeSaved, boolean includeScans, boolean includePasspoints, WifiManager wifiManager, ConnectivityManager connectivityManager, NetworkScoreManager networkScoreManager, Looper currentLooper)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
forceUpdate()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      */
forceScan()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      */
pauseScanning()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      */
resumeScanning()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
startTracking()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 
registerScoreCache()335     private void registerScoreCache() {
336         mNetworkScoreManager.registerNetworkScoreCache(
337                 NetworkKey.TYPE_WIFI,
338                 mScoreCache,
339                 NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
340     }
341 
requestScoresForNetworkKeys(Collection<NetworkKey> keys)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
stopTracking()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 
unregisterScoreCache()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
getAccessPoints()399     public List<AccessPoint> getAccessPoints() {
400         return new ArrayList<>(mAccessPoints);
401     }
402 
getManager()403     public WifiManager getManager() {
404         return mWifiManager;
405     }
406 
isWifiEnabled()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      */
getNumSavedNetworks()417     public int getNumSavedNetworks() {
418         return WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager).size();
419     }
420 
isConnected()421     public boolean isConnected() {
422         return mConnected.get();
423     }
424 
dump(PrintWriter pw)425     public void dump(PrintWriter pw) {
426         pw.println("  - wifi tracker ------");
427         for (AccessPoint accessPoint : getAccessPoints()) {
428             pw.println("  " + accessPoint);
429         }
430     }
431 
handleResume()432     private void handleResume() {
433         mScanResultCache.clear();
434         mSeenBssids.clear();
435         mScanId = 0;
436     }
437 
updateScanResultCache(final List<ScanResult> newResults)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 
getWifiConfigurationForNetworkId( int networkId, final List<WifiConfiguration> configs)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      */
updateAccessPoints()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")
updateAccessPointsLocked(final List<ScanResult> newScanResults, List<WifiConfiguration> configs)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
getCachedOrCreate(ScanResult result, List<AccessPoint> cache)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
getCachedOrCreate(WifiConfiguration config, List<AccessPoint> cache)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 
updateNetworkInfo(NetworkInfo networkInfo)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 
clearAccessPointsAndConditionallyUpdate()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      */
updateNetworkScores()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 
updateWifiState(int state)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 
getCurrentAccessPoints(Context context, boolean includeSaved, boolean includeScans, boolean includePasspoints)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 {
onCapabilitiesChanged(Network network, NetworkCapabilities nc)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 
MainHandler(Looper looper)831         public MainHandler(Looper looper) {
832             super(looper);
833         }
834 
835         @Override
handleMessage(Message msg)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 
removePendingMessages()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 
WorkHandler(Looper looper)892         public WorkHandler(Looper looper) {
893             super(looper);
894         }
895 
896         @Override
handleMessage(Message msg)897         public void handleMessage(Message msg) {
898             synchronized (mLock) {
899                 processMessage(msg);
900             }
901         }
902 
processMessage(Message msg)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 
removePendingMessages()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 
resume()956         void resume() {
957             if (!hasMessages(MSG_SCAN)) {
958                 sendEmptyMessage(MSG_SCAN);
959             }
960         }
961 
forceScan()962         void forceScan() {
963             removeMessages(MSG_SCAN);
964             sendEmptyMessage(MSG_SCAN);
965         }
966 
pause()967         void pause() {
968             mRetry = 0;
969             removeMessages(MSG_SCAN);
970         }
971 
972         @VisibleForTesting
isScanning()973         boolean isScanning() {
974             return hasMessages(MSG_SCAN);
975         }
976 
977         @Override
handleMessage(Message message)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 */
getAll(K key)997         List<V> getAll(K key) {
998             List<V> values = store.get(key);
999             return values != null ? values : Collections.<V>emptyList();
1000         }
1001 
put(K key, V val)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          */
onWifiStateChanged(int state)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          */
onConnectedChanged()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          */
onAccessPointsChanged()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
onAccessPointChanged(AccessPoint accessPoint)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
onLevelChanged(AccessPoint accessPoint)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
copyAndNotifyListeners(boolean notifyListeners)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