diff options
author | Quang Luong <qal@google.com> | 2019-08-20 16:40:17 -0700 |
---|---|---|
committer | Quang Luong <qal@google.com> | 2019-09-23 10:19:29 -0700 |
commit | da9087bbf8eb4f4ce3c013bc7003e5bd63ccb61c (patch) | |
tree | ac7995c346db77bbb2ea610815daf5619c95f913 /libs | |
parent | 5cb2d16dcc76b3f087b48cbf3fb1e0e7f48c8edc (diff) |
Added StandardWifiEntry tracking
StandardWifiEntries (normal Wifi networks represented by an SSID) added
to WifiTracker2 tracking with basic display information in wifi picker.
Test: atest WifiTrackerLibTests, manual visual inspection of wifi
settings
Bug: 70983952
Change-Id: I416a33c9bedf0a2a59f392d4abbe46b788ea78c6
Diffstat (limited to 'libs')
11 files changed, 1065 insertions, 60 deletions
diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java index addab0c08..6f5f53878 100644 --- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java @@ -18,6 +18,8 @@ package com.android.wifitrackerlib; import android.net.wifi.ScanResult; +import androidx.annotation.NonNull; + import java.time.Clock; import java.util.ArrayList; import java.util.HashMap; @@ -55,7 +57,7 @@ public class ScanResultUpdater { /** * Updates scan result list and replaces older scans of the same BSSID. */ - public void update(List<ScanResult> newResults) { + public void update(@NonNull List<ScanResult> newResults) { synchronized (mLock) { evictOldScans(); @@ -71,6 +73,7 @@ public class ScanResultUpdater { /** * Returns all seen scan results merged by BSSID. */ + @NonNull public List<ScanResult> getScanResults() { return getScanResults(mMaxScanAgeMillis); } @@ -79,6 +82,7 @@ public class ScanResultUpdater { * Returns all seen scan results merged by BSSID and newer than maxScanAgeMillis. * maxScanAgeMillis must be less than or equal to the mMaxScanAgeMillis field if it was set. */ + @NonNull public List<ScanResult> getScanResults(long maxScanAgeMillis) throws IllegalArgumentException { if (maxScanAgeMillis > mMaxScanAgeMillis) { throw new IllegalArgumentException( @@ -87,7 +91,7 @@ public class ScanResultUpdater { synchronized (mLock) { List<ScanResult> ageFilteredResults = new ArrayList<>(); for (ScanResult result : mScanResultsByBssid.values()) { - if (mClock.millis() - result.timestamp <= maxScanAgeMillis) { + if (mClock.millis() - result.timestamp / 1000 <= maxScanAgeMillis) { ageFilteredResults.add(result); } } @@ -98,7 +102,7 @@ public class ScanResultUpdater { private void evictOldScans() { synchronized (mLock) { mScanResultsByBssid.entrySet().removeIf((entry) -> - mClock.millis() - entry.getValue().timestamp > mMaxScanAgeMillis); + mClock.millis() - entry.getValue().timestamp / 1000 > mMaxScanAgeMillis); } } } diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/StandardWifiEntry.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/StandardWifiEntry.java new file mode 100644 index 000000000..f4c4f05ed --- /dev/null +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/StandardWifiEntry.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wifitrackerlib; + +import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel; +import static com.android.wifitrackerlib.Utils.getSecurityFromScanResult; + +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import java.util.ArrayList; +import java.util.List; + +/** + * WifiEntry representation of a logical Wi-Fi network, uniquely identified by SSID and security. + * + * This type of WifiEntry can represent both open and saved networks. + */ +class StandardWifiEntry extends WifiEntry { + static final String KEY_PREFIX = "StandardWifiEntry:"; + + private final List<ScanResult> mCurrentScanResults = new ArrayList<>(); + + private final String mKey; + private final String mSsid; + private final @Security int mSecurity; + + private int mLevel = WIFI_LEVEL_UNREACHABLE; + + StandardWifiEntry(@NonNull Handler callbackHandler, @NonNull List<ScanResult> scanResults) + throws IllegalArgumentException { + super(callbackHandler); + + if (scanResults.isEmpty()) { + throw new IllegalArgumentException("Cannot construct with empty ScanResult list!"); + } + final ScanResult firstScan = scanResults.get(0); + mKey = createStandardWifiEntryKey(firstScan); + mSsid = firstScan.SSID; + mSecurity = getSecurityFromScanResult(firstScan); + updateScanResultInfo(scanResults); + } + + @Override + public String getKey() { + return mKey; + } + + @Override + @ConnectedState + public int getConnectedState() { + // TODO(b/70983952): Fill this method in + return CONNECTED_STATE_DISCONNECTED; + } + + @Override + public String getTitle() { + return mSsid; + } + + @Override + public String getSummary() { + // TODO(b/70983952): Fill this method in + return null; + } + + @Override + public int getLevel() { + return mLevel; + } + + @Override + @Security + public int getSecurity() { + // TODO(b/70983952): Fill this method in + return mSecurity; + } + + @Override + public boolean isMetered() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public ConnectedInfo getConnectedInfo() { + // TODO(b/70983952): Fill this method in + return null; + } + + @Override + public boolean canConnect() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void connect() { + // TODO(b/70983952): Fill this method in + } + + @Override + public boolean canDisconnect() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void disconnect() { + // TODO(b/70983952): Fill this method in + } + + @Override + public boolean canForget() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void forget() { + // TODO(b/70983952): Fill this method in + } + + @Override + public boolean canSetPassword() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void setPassword(@NonNull String password) { + // TODO(b/70983952): Fill this method in + } + + @Override + @MeteredChoice + public int getMeteredChoice() { + // TODO(b/70983952): Fill this method in + return METERED_CHOICE_UNMETERED; + } + + @Override + public boolean canSetMeteredChoice() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void setMeteredChoice(int meteredChoice) { + // TODO(b/70983952): Fill this method in + } + + @Override + public boolean canSetPrivacy() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + @Privacy + public int getPrivacy() { + // TODO(b/70983952): Fill this method in + return PRIVACY_RANDOMIZED_MAC; + } + + @Override + public void setPrivacy(int privacy) { + // TODO(b/70983952): Fill this method in + } + + @Override + public boolean isAutoJoinEnabled() { + // TODO(b/70983952): Fill this method in + return true; + } + + @Override + public boolean canSetAutoJoinEnabled() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void setAutoJoinEnabled(boolean enabled) { + // TODO(b/70983952): Fill this method in + } + + @Override + public ProxySettings getProxySettings() { + // TODO(b/70983952): Fill this method in + return null; + } + + @Override + public boolean canSetProxySettings() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void setProxySettings(@NonNull ProxySettings proxySettings) { + // TODO(b/70983952): Fill this method in + } + + @Override + public IpSettings getIpSettings() { + // TODO(b/70983952): Fill this method in + return null; + } + + @Override + public boolean canSetIpSettings() { + // TODO(b/70983952): Fill this method in + return false; + } + + @Override + public void setIpSettings(@NonNull IpSettings ipSettings) { + // TODO(b/70983952): Fill this method in + } + + @WorkerThread + void updateScanResultInfo(@NonNull List<ScanResult> scanResults) + throws IllegalArgumentException { + if (scanResults.isEmpty()) { + throw new IllegalArgumentException("Cannot update with empty ScanResult list!"); + } + + for (ScanResult result : scanResults) { + if (!TextUtils.equals(result.SSID, mSsid)) { + throw new IllegalArgumentException( + "Attempted to update with wrong SSID! Expected: " + + mSsid + ", Actual: " + result.SSID + ", ScanResult: " + result); + } + int security = getSecurityFromScanResult(result); + if (security != mSecurity) { + throw new IllegalArgumentException( + "Attempted to update with wrong security type! Expected: " + + mSecurity + ", Actual: " + security + ", ScanResult: " + result); + } + } + + mCurrentScanResults.clear(); + mCurrentScanResults.addAll(scanResults); + + final ScanResult bestScanResult = getBestScanResultByLevel(mCurrentScanResults); + mLevel = WifiManager.calculateSignalLevel(bestScanResult.level, WifiManager.RSSI_LEVELS); + + notifyOnUpdated(); + } + + static String createStandardWifiEntryKey(ScanResult scan) { + return KEY_PREFIX + scan.SSID + "," + getSecurityFromScanResult(scan); + } +} diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/Utils.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/Utils.java new file mode 100644 index 000000000..aa7f45169 --- /dev/null +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/Utils.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wifitrackerlib; + +import static com.android.wifitrackerlib.WifiEntry.SECURITY_EAP; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_EAP_SUITE_B; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_NONE; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_OWE; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_OWE_TRANSITION; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_PSK; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_PSK_SAE_TRANSITION; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_SAE; +import static com.android.wifitrackerlib.WifiEntry.SECURITY_WEP; + +import static java.util.Comparator.comparingInt; + +import android.net.wifi.ScanResult; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Utility methods for WifiTrackerLib. + */ +class Utils { + // Returns the ScanResult with the best RSSI from a list of ScanResults. + @Nullable + static ScanResult getBestScanResultByLevel(@NonNull List<ScanResult> scanResults) { + if (scanResults.isEmpty()) return null; + + return Collections.max(scanResults, comparingInt(scanResult -> scanResult.level)); + } + + // Returns the SECURITY type of a ScanResult + @WifiEntry.Security + static int getSecurityFromScanResult(@NonNull ScanResult result) { + if (result.capabilities == null) { + return SECURITY_NONE; + } + + if (result.capabilities.contains("WEP")) { + return SECURITY_WEP; + } else if (result.capabilities.contains("PSK+SAE")) { + return SECURITY_PSK_SAE_TRANSITION; + } else if (result.capabilities.contains("SAE")) { + return SECURITY_SAE; + } else if (result.capabilities.contains("PSK")) { + return SECURITY_PSK; + } else if (result.capabilities.contains("EAP_SUITE_B_192")) { + return SECURITY_EAP_SUITE_B; + } else if (result.capabilities.contains("EAP")) { + return SECURITY_EAP; + } else if (result.capabilities.contains("OWE_TRANSITION")) { + return SECURITY_OWE_TRANSITION; + } else if (result.capabilities.contains("OWE")) { + return SECURITY_OWE; + } + return SECURITY_NONE; + } +} diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java index 3742e42e4..3148e6600 100644 --- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java @@ -16,7 +16,11 @@ package com.android.wifitrackerlib; +import android.os.Handler; + +import androidx.annotation.AnyThread; import androidx.annotation.IntDef; +import androidx.annotation.MainThread; import androidx.annotation.NonNull; import java.lang.annotation.Retention; @@ -120,7 +124,12 @@ public abstract class WifiEntry implements Comparable<WifiEntry> { public static final int FREQUENCY_UNKNOWN = -1; // Callback associated with this WifiEntry. Subclasses should call its methods appropriately. - protected WifiEntryCallback mListener; + private WifiEntryCallback mListener; + private Handler mCallbackHandler; + + WifiEntry(@NonNull Handler callbackHandler) throws IllegalArgumentException { + mCallbackHandler = callbackHandler; + } // Info available for all WifiEntries // @@ -299,6 +308,7 @@ public abstract class WifiEntry implements Comparable<WifiEntry> { /** * Listener for changes to the state of the WifiEntry or the result of actions on the WifiEntry. + * These callbacks will be invoked on the main thread. */ public interface WifiEntryCallback { @Retention(RetentionPolicy.SOURCE) @@ -340,21 +350,25 @@ public abstract class WifiEntry implements Comparable<WifiEntry> { * Indicates the state of the WifiEntry has changed and clients may retrieve updates through * the WifiEntry getter methods. */ + @MainThread void onUpdated(); /** * Result of the connect request indicated by the CONNECT_STATUS constants. */ + @MainThread void onConnectResult(@ConnectStatus int status); /** * Result of the disconnect request indicated by the DISCONNECT_STATUS constants. */ + @MainThread void onDisconnectResult(@DisconnectStatus int status); /** * Result of the forget request indicated by the FORGET_STATUS constants. */ + @MainThread void onForgetResult(@ForgetStatus int status); } @@ -372,4 +386,47 @@ public abstract class WifiEntry implements Comparable<WifiEntry> { if (!(other instanceof WifiEntry)) return false; return getKey().equals(((WifiEntry) other).getKey()); } + + @Override + public String toString() { + return new StringBuilder() + .append(getKey()) + .append(",title:") + .append(getTitle()) + .append(",summary:") + .append(getSummary()) + .append(",level:") + .append(getLevel()) + .append(",security:") + .append(getSecurity()) + .toString(); + } + + @AnyThread + protected void notifyOnUpdated() { + if (mListener != null) { + mCallbackHandler.post(() -> mListener.onUpdated()); + } + } + + @AnyThread + protected void notifyOnConnectResult(int status) { + if (mListener != null) { + mCallbackHandler.post(() -> mListener.onConnectResult(status)); + } + } + + @AnyThread + protected void notifyOnDisconnectResult(int status) { + if (mListener != null) { + mCallbackHandler.post(() -> mListener.onDisconnectResult(status)); + } + } + + @AnyThread + protected void notifyOnForgetResult(int status) { + if (mListener != null) { + mCallbackHandler.post(() -> mListener.onForgetResult(status)); + } + } } diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java index 947b26409..a3cfa8af9 100644 --- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java @@ -16,16 +16,24 @@ package com.android.wifitrackerlib; +import static com.android.wifitrackerlib.StandardWifiEntry.createStandardWifiEntryKey; + +import static java.util.stream.Collectors.groupingBy; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; +import android.net.wifi.ScanResult; import android.net.wifi.WifiManager; import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.MainThread; import androidx.annotation.NonNull; @@ -37,12 +45,29 @@ import androidx.lifecycle.OnLifecycleEvent; import java.time.Clock; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Keeps track of the state of Wi-Fi and supplies {@link WifiEntry} for use in Wi-Fi picker lists. * * Clients should use WifiTracker2/WifiEntry for all information regarding Wi-Fi. + * + * This class runs on two threads: + * + * The main thread processes lifecycle events (onStart, onStop), as well as listener callbacks since + * these directly manipulate the UI. + * + * The worker thread is responsible for driving the periodic scan requests and updating internal + * data in reaction to system broadcasts. After a data update, the listener is notified on the main + * thread. + * + * To keep synchronization simple, this means that the vast majority of work is done within the + * worker thread. Synchronized blocks should then be used for updating/accessing only data that is + * consumed by the client listener. */ public class WifiTracker2 implements LifecycleObserver { @@ -66,7 +91,6 @@ public class WifiTracker2 implements LifecycleObserver { private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { /** * TODO (b/70983952): Add the rest of the broadcast handling. - * WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); * WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); * WifiManager.LINK_CONFIGURATION_CHANGED_ACTION); * WifiManager.NETWORK_STATE_CHANGED_ACTION); @@ -78,23 +102,33 @@ public class WifiTracker2 implements LifecycleObserver { String action = intent.getAction(); if (isVerboseLoggingEnabled()) { - Log.i(TAG, "Received broadcast: " + action); + Log.v(TAG, "Received broadcast: " + action); } if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { - updateWifiState(); + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { + mScanner.start(); + } else { + mScanner.stop(); + } + notifyOnWifiStateChanged(); + } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) { + final boolean lastScanSucceeded = + intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); + if (lastScanSucceeded) updateScanResults(); + updateWifiEntries(lastScanSucceeded); + notifyOnWifiEntriesChanged(); } } }; - private final ScanResultUpdater mScanResultUpdater; + private final Scanner mScanner; // Lock object for mWifiEntries private final Object mLock = new Object(); - private int mWifiState; - @GuardedBy("mLock") - private final List<WifiEntry> mWifiEntries; + @GuardedBy("mLock") private final List<WifiEntry> mWifiEntries = new ArrayList<>(); + private final Map<String, StandardWifiEntry> mStandardWifiEntryCache = new HashMap<>(); /** * Constructor for WifiTracker2. @@ -132,10 +166,9 @@ public class WifiTracker2 implements LifecycleObserver { mScanIntervalMillis = scanIntervalMillis; mListener = listener; - mScanResultUpdater = new ScanResultUpdater(clock, maxScanAgeMillis * 2); - - mWifiState = WifiManager.WIFI_STATE_DISABLED; - mWifiEntries = new ArrayList<>(); + mScanResultUpdater = new ScanResultUpdater(clock, + maxScanAgeMillis + scanIntervalMillis); + mScanner = new Scanner(workerHandler.getLooper()); sVerboseLogging = mWifiManager.getVerboseLoggingLevel() > 0; } @@ -147,7 +180,6 @@ public class WifiTracker2 implements LifecycleObserver { @MainThread public void onStart() { // TODO (b/70983952): Register score cache and receivers for network callbacks. - // TODO (b/70983952): Resume scanner here. IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); @@ -156,8 +188,16 @@ public class WifiTracker2 implements LifecycleObserver { // Populate data now so we don't have to wait for the next broadcast. mWorkerHandler.post(() -> { - updateWifiState(); - // TODO (b/70983952): Update other info (eg ScanResults) driven by broadcasts here. + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { + mScanner.start(); + } else { + mScanner.stop(); + } + notifyOnWifiStateChanged(); + + updateScanResults(); + updateWifiEntries(true); + notifyOnWifiEntriesChanged(); }); } @@ -168,7 +208,7 @@ public class WifiTracker2 implements LifecycleObserver { @MainThread public void onStop() { // TODO (b/70983952): Unregister score cache and receivers for network callbacks. - // TODO (b/70983952): Pause scanner here. + mScanner.stop(); mContext.unregisterReceiver(mBroadcastReceiver); } @@ -181,13 +221,15 @@ public class WifiTracker2 implements LifecycleObserver { * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li> * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li> */ + @AnyThread public int getWifiState() { - return mWifiState; + return mWifiManager.getWifiState(); } /** * Returns the WifiEntry representing the current connection. */ + @AnyThread public @Nullable WifiEntry getConnectedWifiEntry() { // TODO (b/70983952): Fill in this method. return null; @@ -199,13 +241,17 @@ public class WifiTracker2 implements LifecycleObserver { * The currently connected entry is omitted and may be accessed through * {@link #getConnectedWifiEntry()} */ + @AnyThread public @NonNull List<WifiEntry> getWifiEntries() { - return mWifiEntries; + synchronized (mLock) { + return new ArrayList<>(mWifiEntries); + } } /** * Returns a list of WifiEntries representing saved networks. */ + @AnyThread public @NonNull List<WifiEntry> getSavedWifiEntries() { // TODO (b/70983952): Fill in this method. return new ArrayList<>(); @@ -214,23 +260,74 @@ public class WifiTracker2 implements LifecycleObserver { /** * Returns a list of WifiEntries representing network subscriptions. */ + @AnyThread public @NonNull List<WifiEntry> getSubscriptionEntries() { // TODO (b/70983952): Fill in this method. return new ArrayList<>(); } + @WorkerThread + private void updateScanResults() { + mScanResultUpdater.update(mWifiManager.getScanResults()); + if (isVerboseLoggingEnabled()) { + Log.v(TAG, "Updated scans: " + Arrays.toString( + mScanResultUpdater.getScanResults(mMaxScanAgeMillis).toArray())); + } + } + + @WorkerThread + private void updateWifiEntries(boolean lastScanSucceeded) { + updateStandardWifiEntryCache(lastScanSucceeded); + // TODO (b/70983952): Update Passpoint/Suggestion entries here. + // updatePasspointWifiEntries(); + // updateCarrierWifiEntries(); + // updateSuggestionWifiEntries(); + synchronized (mLock) { + mWifiEntries.clear(); + mWifiEntries.addAll(mStandardWifiEntryCache.values()); + // mWifiEntries.addAll(mPasspointWifiEntries); + // mWifiEntries.addAll(mCarrierWifiEntries); + // mWifiEntries.addAll(mSuggestionWifiEntries); + Collections.sort(mWifiEntries); + if (isVerboseLoggingEnabled()) { + Log.v(TAG, "Updated WifiEntries: " + Arrays.toString(mWifiEntries.toArray())); + } + } + } + /** - * Updates mWifiState and notifies the listener. + * Updates mStandardWifiEntryCache with fresh scans. */ @WorkerThread - private void updateWifiState() { - mWifiState = mWifiManager.getWifiState(); - notifyOnWifiStateChanged(); + private void updateStandardWifiEntryCache(boolean lastScanSucceeded) { + // If the current scan failed, use results from the previous scan to prevent flicker. + final List<ScanResult> scanResults = mScanResultUpdater.getScanResults( + lastScanSucceeded ? mMaxScanAgeMillis : mMaxScanAgeMillis + mScanIntervalMillis); + final Map<String, StandardWifiEntry> updatedStandardWifiEntries = new HashMap<>(); + + // Group scans by StandardWifiEntry Key + final Map<String, List<ScanResult>> scanResultsByKey = scanResults.stream() + .filter(scanResult -> !TextUtils.isEmpty(scanResult.SSID)) + .collect(groupingBy(StandardWifiEntry::createStandardWifiEntryKey)); + + // Create or get cached StandardWifiEntry by key + for (String key : scanResultsByKey.keySet()) { + StandardWifiEntry entry = mStandardWifiEntryCache.get(key); + if (entry == null) { + entry = new StandardWifiEntry(mMainHandler, scanResultsByKey.get(key)); + } else { + entry.updateScanResultInfo(scanResultsByKey.get(key)); + } + updatedStandardWifiEntries.put(key, entry); + } + mStandardWifiEntryCache.clear(); + mStandardWifiEntryCache.putAll(updatedStandardWifiEntries); } /** * Posts onWifiEntryChanged callback on the main thread. */ + @WorkerThread private void notifyOnWifiEntriesChanged() { if (mListener != null) { mMainHandler.post(mListener::onWifiEntriesChanged); @@ -240,6 +337,7 @@ public class WifiTracker2 implements LifecycleObserver { /** * Posts onWifiStateChanged callback on the main thread. */ + @WorkerThread private void notifyOnWifiStateChanged() { if (mListener != null) { mMainHandler.post(mListener::onWifiStateChanged); @@ -247,6 +345,51 @@ public class WifiTracker2 implements LifecycleObserver { } /** + * Scanner to handle starting scans every SCAN_INTERVAL_MILLIS + */ + private class Scanner extends Handler { + private static final int SCAN_RETRY_TIMES = 3; + + private int mRetry = 0; + + private Scanner(Looper looper) { + super(looper); + } + + @AnyThread + private void start() { + if (isVerboseLoggingEnabled()) { + Log.v(TAG, "Scanner start"); + } + post(this::postScan); + } + + @AnyThread + private void stop() { + if (isVerboseLoggingEnabled()) { + Log.v(TAG, "Scanner stop"); + } + mRetry = 0; + removeCallbacksAndMessages(null); + } + + @WorkerThread + private void postScan() { + if (mWifiManager.startScan()) { + mRetry = 0; + } else if (++mRetry >= SCAN_RETRY_TIMES) { + // TODO(b/70983952): See if toast is needed here + if (isVerboseLoggingEnabled()) { + Log.v(TAG, "Scanner failed to start scan " + mRetry + " times!"); + } + mRetry = 0; + return; + } + postDelayed(this::postScan, mScanIntervalMillis); + } + } + + /** * Listener for changes to the Wi-Fi state or lists of WifiEntries. * * These callbacks must be run on the MainThread. diff --git a/libs/WifiTrackerLib/tests/Android.bp b/libs/WifiTrackerLib/tests/Android.bp index 97eefb7a2..0abd40148 100644 --- a/libs/WifiTrackerLib/tests/Android.bp +++ b/libs/WifiTrackerLib/tests/Android.bp @@ -17,7 +17,6 @@ android_test { srcs: ["src/**/*.java"], static_libs: [ - "android-support-test", "androidx.test.rules", "frameworks-base-testutils", "mockito-target-minus-junit4", @@ -27,10 +26,7 @@ android_test { libs: [ "android.test.mock", - "Robolectric_all-target", ], platform_apis: true, - - test_suites: ["device-tests"], } diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java index b92a764d7..b46153bd2 100644 --- a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java @@ -16,6 +16,8 @@ package com.android.wifitrackerlib; +import static com.android.wifitrackerlib.TestUtils.buildScanResult; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -33,6 +35,7 @@ import java.util.Arrays; import java.util.List; public class ScanResultUpdaterTest { + private static final String SSID = "ssid"; private static final String BSSID_1 = "11:11:11:11:11:11"; private static final String BSSID_2 = "22:22:22:22:22:22"; private static final String BSSID_3 = "33:33:33:33:33:33"; @@ -47,28 +50,13 @@ public class ScanResultUpdaterTest { when(mMockClock.millis()).thenReturn(NOW_MILLIS); } - private static ScanResult buildScanResult(String bssid, long timestampMs) { - return new ScanResult( - null, - bssid, - 0, // hessid - 0, //anqpDomainId - null, // osuProviders - "", // capabilities - 0, - 0, // frequency - timestampMs /* microsecond timestamp */); - } - /** * Verify that scan results of the same BSSID are merged to latest one. */ @Test public void testGetScanResults_mergeSameBssid() { - ScanResult oldResult = buildScanResult( - BSSID_1, 10); - ScanResult newResult = buildScanResult( - BSSID_1, 20); + ScanResult oldResult = buildScanResult(SSID, BSSID_1, 10); + ScanResult newResult = buildScanResult(SSID, BSSID_1, 20); // Add initial scan result. List should have 1 scan. ScanResultUpdater sru = new ScanResultUpdater(mMockClock); @@ -91,8 +79,8 @@ public class ScanResultUpdaterTest { public void testGetScanResults_filtersOldScans() { long maxScanAge = 15_000; - ScanResult oldResult = buildScanResult(BSSID_1, NOW_MILLIS - (maxScanAge + 1)); - ScanResult newResult = buildScanResult(BSSID_2, NOW_MILLIS); + ScanResult oldResult = buildScanResult(SSID, BSSID_1, NOW_MILLIS - (maxScanAge + 1)); + ScanResult newResult = buildScanResult(SSID, BSSID_2, NOW_MILLIS); // Add a new scan result and an out-of-date scan result. ScanResultUpdater sru = new ScanResultUpdater(mMockClock); @@ -124,9 +112,9 @@ public class ScanResultUpdaterTest { public void testConstructor_maxScanAge_filtersOldScans() { ScanResultUpdater sru = new ScanResultUpdater(mMockClock, 15_000); - ScanResult scan1 = buildScanResult(BSSID_1, NOW_MILLIS - 10_000); - ScanResult scan2 = buildScanResult(BSSID_2, NOW_MILLIS - 15_000); - ScanResult scan3 = buildScanResult(BSSID_3, NOW_MILLIS - 20_000); + ScanResult scan1 = buildScanResult(SSID, BSSID_1, NOW_MILLIS - 10_000); + ScanResult scan2 = buildScanResult(SSID, BSSID_2, NOW_MILLIS - 15_000); + ScanResult scan3 = buildScanResult(SSID, BSSID_3, NOW_MILLIS - 20_000); sru.update(Arrays.asList(scan1, scan2, scan3)); @@ -143,9 +131,9 @@ public class ScanResultUpdaterTest { public void testGetScanResults_overridesConstructorMaxScanAge() { ScanResultUpdater sru = new ScanResultUpdater(mMockClock, 15_000); - ScanResult scan1 = buildScanResult(BSSID_1, NOW_MILLIS - 10_000); - ScanResult scan2 = buildScanResult(BSSID_2, NOW_MILLIS - 15_000); - ScanResult scan3 = buildScanResult(BSSID_3, NOW_MILLIS - 20_000); + ScanResult scan1 = buildScanResult(SSID, BSSID_1, NOW_MILLIS - 10_000); + ScanResult scan2 = buildScanResult(SSID, BSSID_2, NOW_MILLIS - 15_000); + ScanResult scan3 = buildScanResult(SSID, BSSID_3, NOW_MILLIS - 20_000); sru.update(Arrays.asList(scan1, scan2, scan3)); diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/StandardWifiEntryTest.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/StandardWifiEntryTest.java new file mode 100644 index 000000000..8c07cec83 --- /dev/null +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/StandardWifiEntryTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wifitrackerlib; + +import static com.android.wifitrackerlib.TestUtils.buildScanResult; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; + +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.test.TestLooper; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; + +public class StandardWifiEntryTest { + public static final int GOOD_RSSI = -50; + public static final int OKAY_RSSI = -60; + public static final int BAD_RSSI = -70; + + @Mock private WifiEntry.WifiEntryCallback mMockListener; + + private TestLooper mTestLooper; + private Handler mTestHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + mTestHandler = new Handler(mTestLooper.getLooper()); + } + + /** + * Tests that constructing with an empty list of scans throws an exception + */ + @Test + public void testConstructor_emptyScanList_throwsException() { + try { + new StandardWifiEntry(mTestHandler, new ArrayList<>()); + fail("Empty scan list should have thrown exception"); + } catch (IllegalArgumentException e) { + // Test succeeded + } + } + + /** + * Tests that constructing with a list of scans with differing SSIDs throws an exception + */ + @Test + public void testConstructor_mismatchedSsids_throwsException() { + try { + new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid0", "bssid0", 0, GOOD_RSSI), + buildScanResult("ssid1", "bssid1", 0, GOOD_RSSI))); + fail("Scan list with different SSIDs should have thrown exception"); + } catch (IllegalArgumentException e) { + // Test succeeded + } + } + + /** + * Tests that the level is set to the level of the strongest scan + */ + @Test + public void testConstructor_setsBestLevel() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid", "bssid0", 0, GOOD_RSSI), + buildScanResult("ssid", "bssid1", 0, OKAY_RSSI), + buildScanResult("ssid", "bssid2", 0, BAD_RSSI))); + + assertThat(entry.getLevel()).isEqualTo( + WifiManager.calculateSignalLevel(GOOD_RSSI, WifiManager.RSSI_LEVELS)); + } + + /** + * Tests that the security is set to the security capabilities of the scan + */ + @Test + public void testConstructor_setsSecurity() { + final ScanResult unsecureScan = buildScanResult("ssid", "bssid", 0, GOOD_RSSI); + final ScanResult secureScan = buildScanResult("ssid", "bssid", 0, GOOD_RSSI); + secureScan.capabilities = "EAP"; + + final StandardWifiEntry unsecureEntry = new StandardWifiEntry(mTestHandler, + Arrays.asList(unsecureScan)); + final StandardWifiEntry secureEntry = new StandardWifiEntry(mTestHandler, + Arrays.asList(secureScan)); + + assertThat(unsecureEntry.getSecurity()).isEqualTo(WifiEntry.SECURITY_NONE); + assertThat(secureEntry.getSecurity()).isEqualTo(WifiEntry.SECURITY_EAP); + } + + /** + * Tests that updating with an empty list of scans throws an exception. + */ + @Test + public void testUpdateScanResultInfo_emptyScanList_throwsException() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid", "bssid", 0, GOOD_RSSI)) + ); + + try { + entry.updateScanResultInfo(new ArrayList<>()); + fail("Empty scan list should have thrown exception"); + } catch (IllegalArgumentException e) { + // Test succeeded + } + } + + /** + * Tests that updating with a list of scans with differing SSIDs throws an exception + */ + @Test + public void testUpdateScanResultInfo_mismatchedSsids_throwsException() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid0", "bssid0", 0, GOOD_RSSI))); + + try { + entry.updateScanResultInfo(Arrays.asList( + buildScanResult("ssid1", "bssid1", 0, GOOD_RSSI))); + fail("Scan list with different SSIDs should have thrown exception"); + } catch (IllegalArgumentException e) { + // Test succeeded + } + } + + /** + * Tests that updating with a list of scans with differing security types throws an exception. + */ + @Test + public void testUpdateScanResultInfo_mismatchedSecurity_throwsException() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid0", "bssid0", 0, GOOD_RSSI))); + + try { + final ScanResult differentSecurityScan = + buildScanResult("ssid0", "bssid0", 0, GOOD_RSSI); + differentSecurityScan.capabilities = "EAP"; + entry.updateScanResultInfo(Arrays.asList(differentSecurityScan)); + fail("Scan list with different SSIDs should have thrown exception"); + } catch (IllegalArgumentException e) { + // Test succeeded + } + } + + /** + * Tests that the listener is notified after an update to the scan results + */ + @Test + public void testUpdateScanResultInfo_notifiesListener() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid", "bssid", 0))); + entry.setListener(mMockListener); + + entry.updateScanResultInfo(Arrays.asList(buildScanResult("ssid", "bssid", 1))); + mTestLooper.dispatchAll(); + + verify(mMockListener).onUpdated(); + } + + /** + * Tests that the level is updated after an update to the scan results + */ + @Test + public void testUpdateScanResultInfo_updatesLevel() { + final StandardWifiEntry entry = new StandardWifiEntry(mTestHandler, Arrays.asList( + buildScanResult("ssid", "bssid", 0, BAD_RSSI))); + + assertThat(entry.getLevel()).isEqualTo( + WifiManager.calculateSignalLevel(BAD_RSSI, WifiManager.RSSI_LEVELS)); + + entry.updateScanResultInfo(Arrays.asList(buildScanResult("ssid", "bssid", 0, GOOD_RSSI))); + + assertThat(entry.getLevel()).isEqualTo( + WifiManager.calculateSignalLevel(GOOD_RSSI, WifiManager.RSSI_LEVELS)); + } +} diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/TestUtils.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/TestUtils.java new file mode 100644 index 000000000..547a5721e --- /dev/null +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/TestUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wifitrackerlib; + +import android.net.wifi.ScanResult; + +/** + * Utility methods for testing purposes. + */ +class TestUtils { + /** + * Creates a mock scan result with SSID, BSSID, and timestamp. + */ + static ScanResult buildScanResult(String ssid, String bssid, long timestampMillis) { + final ScanResult result = new ScanResult(); + result.SSID = ssid; + result.BSSID = bssid; + result.timestamp = timestampMillis * 1000; + return result; + } + + static ScanResult buildScanResult(String ssid, String bssid, long timestampMillis, int rssi) { + final ScanResult result = buildScanResult(ssid, bssid, timestampMillis); + result.level = rssi; + return result; + } +} diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/UtilsTest.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/UtilsTest.java new file mode 100644 index 000000000..5b33d0ca6 --- /dev/null +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/UtilsTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wifitrackerlib; + +import static com.android.wifitrackerlib.TestUtils.buildScanResult; +import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.wifi.ScanResult; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +public class UtilsTest { + + @Test + public void testGetBestScanResult_emptyList_returnsNull() { + assertThat(getBestScanResultByLevel(new ArrayList<>())).isNull(); + } + + @Test + public void testGetBestScanResult_returnsBestRssiScan() { + final ScanResult bestResult = buildScanResult("ssid", "bssid", 0, -50); + final ScanResult okayResult = buildScanResult("ssid", "bssid", 0, -60); + final ScanResult badResult = buildScanResult("ssid", "bssid", 0, -70); + + assertThat(getBestScanResultByLevel(Arrays.asList(bestResult, okayResult, badResult))) + .isEqualTo(bestResult); + } + + @Test + public void testGetBestScanResult_singleScan_returnsScan() { + final ScanResult scan = buildScanResult("ssid", "bssid", 0, -50); + + assertThat(getBestScanResultByLevel(Arrays.asList(scan))).isEqualTo(scan); + } +} diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java index 410f18277..8cbb04dab 100644 --- a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java @@ -16,6 +16,9 @@ package com.android.wifitrackerlib; +import static com.android.wifitrackerlib.StandardWifiEntry.createStandardWifiEntryKey; +import static com.android.wifitrackerlib.TestUtils.buildScanResult; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -28,6 +31,7 @@ import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; +import android.net.wifi.ScanResult; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.test.TestLooper; @@ -41,9 +45,17 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.time.Clock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class WifiTracker2Test { + private static final long START_MILLIS = 123_456_789; + + private static final long MAX_SCAN_AGE_MILLIS = 15_000; + private static final long SCAN_INTERVAL_MILLIS = 10_000; + @Mock private Lifecycle mMockLifecycle; @Mock private Context mMockContext; @Mock private WifiManager mMockWifiManager; @@ -52,7 +64,7 @@ public class WifiTracker2Test { @Mock private Clock mMockClock; @Mock private WifiTracker2.WifiTrackerCallback mMockWifiTrackerCallback; - private TestLooper mTestLooper = new TestLooper(); + private TestLooper mTestLooper; private final ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); @@ -67,14 +79,19 @@ public class WifiTracker2Test { testHandler, testHandler, mMockClock, - 15_000, - 10_000, + MAX_SCAN_AGE_MILLIS, + SCAN_INTERVAL_MILLIS, mMockWifiTrackerCallback); } @Before public void setUp() { MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + + when(mMockWifiManager.getScanResults()).thenReturn(new ArrayList<>()); + when(mMockClock.millis()).thenReturn(START_MILLIS); } /** @@ -82,17 +99,19 @@ public class WifiTracker2Test { */ @Test public void testWifiStateChangeBroadcast_updatesWifiState() { - WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); wifiTracker2.onStart(); verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any(), any(), any()); + // Set the wifi state to disabled when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_DISABLED); mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION)); assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_DISABLED); + // Change the wifi state to enabled when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED); mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION)); @@ -106,7 +125,7 @@ public class WifiTracker2Test { */ @Test public void testWifiStateChangeBroadcast_NotifiesListener() { - WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); wifiTracker2.onStart(); verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any(), any(), any()); @@ -123,13 +142,16 @@ public class WifiTracker2Test { */ @Test public void testOnStart_setsWifiState() { - WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + + // Set the wifi state to disabled when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_DISABLED); wifiTracker2.onStart(); mTestLooper.dispatchAll(); assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_DISABLED); + // Change the wifi state to enabled wifiTracker2.onStop(); when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED); wifiTracker2.onStart(); @@ -137,4 +159,152 @@ public class WifiTracker2Test { assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_ENABLED); } + + /** + * Tests that receiving a scan results available broadcast notifies the listener. + */ + @Test + public void testScanResultsAvailableAction_notifiesListener() { + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + mTestLooper.dispatchAll(); + + verify(mMockWifiTrackerCallback, atLeastOnce()).onWifiEntriesChanged(); + } + + /** + * Tests that an empty list of WifiEntries is returned if no scans are available. + */ + @Test + public void testGetWifiEntries_noScans_emptyList() { + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + when(mMockWifiManager.getScanResults()).thenReturn(new ArrayList<>()); + + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + + assertThat(wifiTracker2.getWifiEntries()).isEmpty(); + } + + + /** + * Tests that a StandardWifiEntry is returned by getWifiEntries() for each non-null, non-empty + * SSID/Security pair in the tracked scan results. + */ + @Test + public void testGetWifiEntries_wifiNetworkEntries_createdForEachSsidAndSecurityPair() { + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + final ScanResult openNetwork = buildScanResult("Open Network", "bssid0", START_MILLIS); + final ScanResult openNetworkDup = buildScanResult("Open Network", "bssid1", START_MILLIS); + final ScanResult secureNetwork = buildScanResult("Secure Network", "bssid2", START_MILLIS); + secureNetwork.capabilities = "EAP"; + + when(mMockWifiManager.getScanResults()).thenReturn(Arrays.asList( + openNetwork, + openNetworkDup, + secureNetwork, + // Ignore null and empty SSIDs + buildScanResult(null, "bssidNull", START_MILLIS), + buildScanResult("", "bssidEmpty", START_MILLIS))); + + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + List<String> seenKeys = new ArrayList<>(); + for (WifiEntry wifiEntry : wifiTracker2.getWifiEntries()) { + seenKeys.add(wifiEntry.getKey()); + } + + assertThat(seenKeys).containsExactly( + createStandardWifiEntryKey(openNetwork), + createStandardWifiEntryKey(secureNetwork)); + } + + /** + * Tests that old WifiEntries are timed out if their scans are older than the max scan age. + */ + @Test + public void testGetWifiEntries_wifiNetworkEntries_oldEntriesTimedOut() { + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + // Initial entries + when(mMockWifiManager.getScanResults()).thenReturn(Arrays.asList( + buildScanResult("ssid0", "bssid0", START_MILLIS), + buildScanResult("ssid1", "bssid1", START_MILLIS), + buildScanResult("ssid2", "bssid2", START_MILLIS), + buildScanResult("ssid3", "bssid3", START_MILLIS), + buildScanResult("ssid4", "bssid4", START_MILLIS))); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + + // Advance clock to max scan age. Entries should still be valid. + when(mMockClock.millis()).thenReturn(START_MILLIS + MAX_SCAN_AGE_MILLIS); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + assertThat(wifiTracker2.getWifiEntries()).isNotEmpty(); + + + // Advance the clock to time out old entries + when(mMockClock.millis()).thenReturn(START_MILLIS + MAX_SCAN_AGE_MILLIS + 1); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + + // All entries timed out + assertThat(wifiTracker2.getWifiEntries()).isEmpty(); + } + + /** + * Tests that a failed scan will result in extending the max scan age by the scan interval. + * This is to allow the WifiEntry list to stay stable and not clear out if a single scan fails. + */ + @Test + public void testGetWifiEntries_wifiNetworkEntries_useOldEntriesOnFailedScan() { + final WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + // Initial entries + when(mMockWifiManager.getScanResults()).thenReturn(Arrays.asList( + buildScanResult("ssid0", "bssid0", START_MILLIS), + buildScanResult("ssid1", "bssid1", START_MILLIS), + buildScanResult("ssid2", "bssid2", START_MILLIS), + buildScanResult("ssid3", "bssid3", START_MILLIS), + buildScanResult("ssid4", "bssid4", START_MILLIS))); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + final List<WifiEntry> previousEntries = wifiTracker2.getWifiEntries(); + + // Advance the clock to time out old entries and simulate failed scan + when(mMockClock.millis()) + .thenReturn(START_MILLIS + MAX_SCAN_AGE_MILLIS + SCAN_INTERVAL_MILLIS); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + .putExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)); + + // Failed scan should result in old WifiEntries still being shown + assertThat(previousEntries).containsExactlyElementsIn(wifiTracker2.getWifiEntries()); + + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + .putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true)); + + // Successful scan should time out old entries. + assertThat(wifiTracker2.getWifiEntries()).isEmpty(); + } } |