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