From a0a007f771192ed6f09adf28219f209876cac6bd Mon Sep 17 00:00:00 2001 From: Quang Luong Date: Wed, 3 Jul 2019 10:58:54 -0700 Subject: Created WifiTracker2 and WifiEntry Created WifiTracker2 and WifiEntry to replace WifiTracker/AccessPoint. Implemented WifiTracker2 wifi state change callbacks. Use Clock instead of SystemClock in ScanResultUpdater. Test: atest WifiTracker2Test, atest ScanResultUpdaterTest Bug: 70983952 Change-Id: Iac1c73b54b5df9d50aed0480cbf483be66c7eeb9 --- libs/WifiTrackerLib/Android.bp | 5 + .../android/wifitrackerlib/ScanResultUpdater.java | 20 +- .../src/com/android/wifitrackerlib/WifiEntry.java | 375 +++++++++++++++++++++ .../com/android/wifitrackerlib/WifiTracker2.java | 272 +++++++++++++++ libs/WifiTrackerLib/tests/Android.bp | 8 + .../wifitrackerlib/ScanResultUpdaterTest.java | 73 ++-- .../android/wifitrackerlib/WifiTracker2Test.java | 140 ++++++++ 7 files changed, 849 insertions(+), 44 deletions(-) create mode 100644 libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java create mode 100644 libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java create mode 100644 libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java (limited to 'libs') diff --git a/libs/WifiTrackerLib/Android.bp b/libs/WifiTrackerLib/Android.bp index 501a956d2..412f182dd 100644 --- a/libs/WifiTrackerLib/Android.bp +++ b/libs/WifiTrackerLib/Android.bp @@ -1,4 +1,9 @@ android_library { name: "WifiTrackerLib", srcs: ["src/**/*.java"], + static_libs: [ + "androidx.preference_preference", + ], + + min_sdk_version: "21", } diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java index 6df4f266c..addab0c08 100644 --- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/ScanResultUpdater.java @@ -17,8 +17,8 @@ package com.android.wifitrackerlib; import android.net.wifi.ScanResult; -import android.os.SystemClock; +import java.time.Clock; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -30,22 +30,26 @@ import java.util.List; */ public class ScanResultUpdater { private HashMap mScanResultsByBssid = new HashMap<>(); - private long mMaxScanAgeMillis; - private Object mLock = new Object(); + private final long mMaxScanAgeMillis; + private final Object mLock = new Object(); + private final Clock mClock; /** * Creates a ScanResultUpdater with no max scan age. + * + * @param clock Elapsed real time Clock to compare with ScanResult timestamps. */ - public ScanResultUpdater() { - this(Long.MAX_VALUE); + public ScanResultUpdater(Clock clock) { + this(clock, Long.MAX_VALUE); } /** * Creates a ScanResultUpdater with a max scan age in milliseconds. Scans older than this limit * will be pruned upon update/retrieval to keep the size of the scan list down. */ - public ScanResultUpdater(long maxScanAgeMillis) { + public ScanResultUpdater(Clock clock, long maxScanAgeMillis) { mMaxScanAgeMillis = maxScanAgeMillis; + mClock = clock; } /** @@ -83,7 +87,7 @@ public class ScanResultUpdater { synchronized (mLock) { List ageFilteredResults = new ArrayList<>(); for (ScanResult result : mScanResultsByBssid.values()) { - if (SystemClock.elapsedRealtime() - result.timestamp <= maxScanAgeMillis) { + if (mClock.millis() - result.timestamp <= maxScanAgeMillis) { ageFilteredResults.add(result); } } @@ -94,7 +98,7 @@ public class ScanResultUpdater { private void evictOldScans() { synchronized (mLock) { mScanResultsByBssid.entrySet().removeIf((entry) -> - SystemClock.elapsedRealtime() - entry.getValue().timestamp > mMaxScanAgeMillis); + mClock.millis() - entry.getValue().timestamp > mMaxScanAgeMillis); } } } diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java new file mode 100644 index 000000000..3742e42e4 --- /dev/null +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiEntry.java @@ -0,0 +1,375 @@ +/* + * 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 androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * Abstract base class for an entry representing a Wi-Fi network in a Wi-Fi picker. + * + * Clients implementing a Wi-Fi picker should receive WifiEntry objects from WifiTracker2, and rely + * on the given API for all user-displayable information and actions on the represented network. + */ +public abstract class WifiEntry implements Comparable { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + SECURITY_NONE, + SECURITY_WEP, + SECURITY_PSK, + SECURITY_EAP, + SECURITY_OWE, + SECURITY_SAE, + SECURITY_EAP_SUITE_B, + SECURITY_PSK_SAE_TRANSITION, + SECURITY_OWE_TRANSITION, + SECURITY_MAX_VAL + }) + + public @interface Security {} + + public static final int SECURITY_NONE = 0; + public static final int SECURITY_WEP = 1; + public static final int SECURITY_PSK = 2; + public static final int SECURITY_EAP = 3; + public static final int SECURITY_OWE = 4; + public static final int SECURITY_SAE = 5; + public static final int SECURITY_EAP_SUITE_B = 6; + public static final int SECURITY_PSK_SAE_TRANSITION = 7; + public static final int SECURITY_OWE_TRANSITION = 8; + public static final int SECURITY_MAX_VAL = 9; // Has to be the last + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + CONNECTED_STATE_DISCONNECTED, + CONNECTED_STATE_CONNECTED, + CONNECTED_STATE_CONNECTING + }) + + public @interface ConnectedState {} + + public static final int CONNECTED_STATE_DISCONNECTED = 0; + public static final int CONNECTED_STATE_CONNECTING = 1; + public static final int CONNECTED_STATE_CONNECTED = 2; + + // Wi-Fi signal levels for displaying signal strength. + public static final int WIFI_LEVEL_MIN = 0; + public static final int WIFI_LEVEL_MAX = 4; + public static final int WIFI_LEVEL_UNREACHABLE = -1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + METERED_CHOICE_AUTO, + METERED_CHOICE_METERED, + METERED_CHOICE_UNMETERED, + METERED_CHOICE_UNKNOWN + }) + + public @interface MeteredChoice {} + + // User's choice whether to treat a network as metered. + public static final int METERED_CHOICE_AUTO = 0; + public static final int METERED_CHOICE_METERED = 1; + public static final int METERED_CHOICE_UNMETERED = 2; + public static final int METERED_CHOICE_UNKNOWN = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + PRIVACY_DEVICE_MAC, + PRIVACY_RANDOMIZED_MAC, + PRIVACY_UNKNOWN + }) + + public @interface Privacy {} + + public static final int PRIVACY_DEVICE_MAC = 0; + public static final int PRIVACY_RANDOMIZED_MAC = 1; + public static final int PRIVACY_UNKNOWN = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FREQUENCY_2_4_GHZ, + FREQUENCY_5_GHZ, + FREQUENCY_6_GHZ, + FREQUENCY_UNKNOWN + }) + + public @interface Frequency {} + + public static final int FREQUENCY_2_4_GHZ = 2_400; + public static final int FREQUENCY_5_GHZ = 5_000; + public static final int FREQUENCY_6_GHZ = 6_000; + public static final int FREQUENCY_UNKNOWN = -1; + + // Callback associated with this WifiEntry. Subclasses should call its methods appropriately. + protected WifiEntryCallback mListener; + + // Info available for all WifiEntries // + + /** The unique key defining a WifiEntry */ + public abstract String getKey(); + + /** Returns connection state of the network defined by the CONNECTED_STATE constants */ + @ConnectedState + public abstract int getConnectedState(); + + /** Returns the display title. This is most commonly the SSID of a network. */ + public abstract String getTitle(); + + /** Returns the display summary */ + public abstract String getSummary(); + + /** + * Returns the signal strength level within [WIFI_LEVEL_MIN, WIFI_LEVEL_MAX]. + * A value of WIFI_LEVEL_UNREACHABLE indicates an out of range network. + */ + public abstract int getLevel(); + + /** Returns the security type defined by the SECURITY constants */ + @Security + public abstract int getSecurity(); + + /** + * Indicates when a network is metered or the user marked the network as metered. + */ + public abstract boolean isMetered(); + + /** + * Returns the ConnectedInfo object pertaining to an active connection. + * + * Returns null if getConnectedState() != CONNECTED_STATE_CONNECTED. + */ + public abstract ConnectedInfo getConnectedInfo(); + + /** + * Info associated with the active connection. + */ + public static class ConnectedInfo { + @Frequency + public int frequencyMhz; + public List dnsServers; + public int linkSpeedMbps; + public String ipAddress; + public List ipv6Addresses; + public String gateway; + public String subnetMask; + } + + // User actions on a network + + /** Returns whether the entry should show a connect option */ + public abstract boolean canConnect(); + /** Connects to the network */ + public abstract void connect(); + + /** Returns whether the entry should show a disconnect option */ + public abstract boolean canDisconnect(); + /** Disconnects from the network */ + public abstract void disconnect(); + + /** Returns whether the entry should show a forget option */ + public abstract boolean canForget(); + /** Forgets the network */ + public abstract void forget(); + + // Modifiable settings + + /** Returns whether the entry should show a password input */ + public abstract boolean canSetPassword(); + /** Sets the user's password to a network */ + public abstract void setPassword(@NonNull String password); + + /** + * Returns the user's choice whether to treat a network as metered, + * defined by the METERED_CHOICE constants + */ + @MeteredChoice + public abstract int getMeteredChoice(); + /** Returns whether the entry should let the user choose the metered treatment of a network */ + public abstract boolean canSetMeteredChoice(); + /** + * Sets the user's choice for treating a network as metered, + * defined by the METERED_CHOICE constants + */ + public abstract void setMeteredChoice(@MeteredChoice int meteredChoice); + + /** Returns whether the entry should let the user choose the MAC randomization setting */ + public abstract boolean canSetPrivacy(); + /** Returns the MAC randomization setting defined by the PRIVACY constants */ + @Privacy + public abstract int getPrivacy(); + /** Sets the user's choice for MAC randomization defined by the PRIVACY constants */ + public abstract void setPrivacy(@Privacy int privacy); + + /** Returns whether the network has auto-join enabled */ + public abstract boolean isAutoJoinEnabled(); + /** Returns whether the user can enable/disable auto-join */ + public abstract boolean canSetAutoJoinEnabled(); + /** Sets whether a network will be auto-joined or not */ + public abstract void setAutoJoinEnabled(boolean enabled); + + /** Returns the ProxySettings for the network */ + public abstract ProxySettings getProxySettings(); + /** Returns whether the user can modify the ProxySettings for the network */ + public abstract boolean canSetProxySettings(); + /** Sets the ProxySettinsg for the network */ + public abstract void setProxySettings(@NonNull ProxySettings proxySettings); + + /** Returns the IpSettings for the network */ + public abstract IpSettings getIpSettings(); + /** Returns whether the user can set the IpSettings for the network */ + public abstract boolean canSetIpSettings(); + /** Sets the IpSettings for the network */ + public abstract void setIpSettings(@NonNull IpSettings ipSettings); + + /** + * Data class used for proxy settings + */ + public static class ProxySettings { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TYPE_NONE, + TYPE_MANUAL, + TYPE_PROXY_AUTOCONFIG + }) + + public @interface Type {} + + public static final int TYPE_NONE = 0; + public static final int TYPE_MANUAL = 1; + public static final int TYPE_PROXY_AUTOCONFIG = 2; + + @Type + public int type; + public String hostname; + public String port; + public String bypassAddresses; + public String pacUrl; + } + + /** + * Data class used for IP settings + */ + public static class IpSettings { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TYPE_DCHP, + TYPE_STATIC + }) + + public @interface Type {} + + public static final int TYPE_DCHP = 0; + public static final int TYPE_STATIC = 1; + + @Type + public int type; + public String ipAddress; + public String gateway; + public int prefixLength; + public String dns1; + public String dns2; + } + + /** + * Sets the callback listener for WifiEntryCallback methods. + * Subsequent calls will overwrite the previous listener. + */ + public void setListener(WifiEntryCallback listener) { + mListener = listener; + } + + /** + * Listener for changes to the state of the WifiEntry or the result of actions on the WifiEntry. + */ + public interface WifiEntryCallback { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + CONNECT_STATUS_SUCCESS, + CONNECT_STATUS_FAILURE_NO_PASSWORD, + CONNECT_STATUS_FAILURE_UNKNOWN + }) + + public @interface ConnectStatus {} + + int CONNECT_STATUS_SUCCESS = 0; + int CONNECT_STATUS_FAILURE_NO_PASSWORD = 1; + int CONNECT_STATUS_FAILURE_UNKNOWN = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + CONNECT_STATUS_SUCCESS, + CONNECT_STATUS_FAILURE_UNKNOWN + }) + + public @interface DisconnectStatus {} + + int DISCONNECT_STATUS_SUCCESS = 0; + int DISCONNECT_STATUS_FAILURE_UNKNOWN = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FORGET_STATUS_SUCCESS, + FORGET_STATUS_FAILURE_UNKNOWN + }) + + public @interface ForgetStatus {} + + int FORGET_STATUS_SUCCESS = 0; + int FORGET_STATUS_FAILURE_UNKNOWN = 1; + + /** + * Indicates the state of the WifiEntry has changed and clients may retrieve updates through + * the WifiEntry getter methods. + */ + void onUpdated(); + + /** + * Result of the connect request indicated by the CONNECT_STATUS constants. + */ + void onConnectResult(@ConnectStatus int status); + + /** + * Result of the disconnect request indicated by the DISCONNECT_STATUS constants. + */ + void onDisconnectResult(@DisconnectStatus int status); + + /** + * Result of the forget request indicated by the FORGET_STATUS constants. + */ + void onForgetResult(@ForgetStatus int status); + } + + // TODO (b/70983952) Come up with a sorting scheme that does the right thing. + @Override + public int compareTo(@NonNull WifiEntry other) { + if (getLevel() > other.getLevel()) return -1; + if (getLevel() < other.getLevel()) return 1; + + return getTitle().compareTo(other.getTitle()); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof WifiEntry)) return false; + return getKey().equals(((WifiEntry) other).getKey()); + } +} diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java new file mode 100644 index 000000000..947b26409 --- /dev/null +++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiTracker2.java @@ -0,0 +1,272 @@ +/* + * 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.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.WifiManager; +import android.os.Handler; +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.List; + +/** + * 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. + */ +public class WifiTracker2 implements LifecycleObserver { + + private static final String TAG = "WifiTracker2"; + + public static boolean sVerboseLogging; + + private static boolean isVerboseLoggingEnabled() { + return WifiTracker2.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE); + } + + private final Context mContext; + private final WifiManager mWifiManager; + private final ConnectivityManager mConnectivityManager; + private final NetworkScoreManager mNetworkScoreManager; + private final Handler mMainHandler; + private final Handler mWorkerHandler; + private final long mMaxScanAgeMillis; + private final long mScanIntervalMillis; + private final WifiTrackerCallback mListener; + 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); + * WifiManager.RSSI_CHANGED_ACTION); + */ + @Override + @WorkerThread + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (isVerboseLoggingEnabled()) { + Log.i(TAG, "Received broadcast: " + action); + } + + if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { + updateWifiState(); + } + } + }; + + private final ScanResultUpdater mScanResultUpdater; + + // Lock object for mWifiEntries + private final Object mLock = new Object(); + + private int mWifiState; + @GuardedBy("mLock") + private final List mWifiEntries; + + /** + * Constructor for WifiTracker2. + * + * @param lifecycle Lifecycle this is tied to for lifecycle callbacks. + * @param context Context to retrieve WifiManager and resource strings. + * @param wifiManager Provides all Wi-Fi info. + * @param connectivityManager Provides network info. + * @param networkScoreManager Provides network scores for network badging. + * @param mainHandler Handler for processing listener callbacks. + * @param workerHandler Handler for processing all broadcasts and running the Scanner. + * @param clock Clock used for evaluating the age of scans + * @param maxScanAgeMillis Max age for tracked WifiEntries. + * @param scanIntervalMillis Interval between initiating scans. + * @param listener WifiTrackerCallback listening on changes to WifiTracker2 data. + */ + public WifiTracker2(@NonNull Lifecycle lifecycle, @NonNull Context context, + @NonNull WifiManager wifiManager, + @NonNull ConnectivityManager connectivityManager, + @NonNull NetworkScoreManager networkScoreManager, + @NonNull Handler mainHandler, + @NonNull Handler workerHandler, + @NonNull Clock clock, + long maxScanAgeMillis, + long scanIntervalMillis, + @Nullable WifiTrackerCallback listener) { + lifecycle.addObserver(this); + mContext = context; + mWifiManager = wifiManager; + mConnectivityManager = connectivityManager; + mNetworkScoreManager = networkScoreManager; + mMainHandler = mainHandler; + mWorkerHandler = workerHandler; + mMaxScanAgeMillis = maxScanAgeMillis; + mScanIntervalMillis = scanIntervalMillis; + mListener = listener; + + mScanResultUpdater = new ScanResultUpdater(clock, maxScanAgeMillis * 2); + + mWifiState = WifiManager.WIFI_STATE_DISABLED; + mWifiEntries = new ArrayList<>(); + + sVerboseLogging = mWifiManager.getVerboseLoggingLevel() > 0; + } + + /** + * Registers the broadcast receiver and network callbacks and starts the scanning mechanism. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_START) + @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); + mContext.registerReceiver(mBroadcastReceiver, filter, + /* broadcastPermission */ null, mWorkerHandler); + + // 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. + }); + } + + /** + * Unregisters the broadcast receiver, network callbacks, and pauses the scanning mechanism. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + @MainThread + public void onStop() { + // TODO (b/70983952): Unregister score cache and receivers for network callbacks. + // TODO (b/70983952): Pause scanner here. + mContext.unregisterReceiver(mBroadcastReceiver); + } + + /** + * Returns the state of Wi-Fi as one of the following values. + * + *
  • {@link WifiManager#WIFI_STATE_DISABLED}
  • + *
  • {@link WifiManager#WIFI_STATE_ENABLED}
  • + *
  • {@link WifiManager#WIFI_STATE_DISABLING}
  • + *
  • {@link WifiManager#WIFI_STATE_ENABLING}
  • + *
  • {@link WifiManager#WIFI_STATE_UNKNOWN}
  • + */ + public int getWifiState() { + return mWifiState; + } + + /** + * Returns the WifiEntry representing the current connection. + */ + public @Nullable WifiEntry getConnectedWifiEntry() { + // TODO (b/70983952): Fill in this method. + return null; + } + + /** + * Returns a list of in-range WifiEntries. + * + * The currently connected entry is omitted and may be accessed through + * {@link #getConnectedWifiEntry()} + */ + public @NonNull List getWifiEntries() { + return mWifiEntries; + } + + /** + * Returns a list of WifiEntries representing saved networks. + */ + public @NonNull List getSavedWifiEntries() { + // TODO (b/70983952): Fill in this method. + return new ArrayList<>(); + } + + /** + * Returns a list of WifiEntries representing network subscriptions. + */ + public @NonNull List getSubscriptionEntries() { + // TODO (b/70983952): Fill in this method. + return new ArrayList<>(); + } + + /** + * Updates mWifiState and notifies the listener. + */ + @WorkerThread + private void updateWifiState() { + mWifiState = mWifiManager.getWifiState(); + notifyOnWifiStateChanged(); + } + + /** + * Posts onWifiEntryChanged callback on the main thread. + */ + private void notifyOnWifiEntriesChanged() { + if (mListener != null) { + mMainHandler.post(mListener::onWifiEntriesChanged); + } + } + + /** + * Posts onWifiStateChanged callback on the main thread. + */ + private void notifyOnWifiStateChanged() { + if (mListener != null) { + mMainHandler.post(mListener::onWifiStateChanged); + } + } + + /** + * Listener for changes to the Wi-Fi state or lists of WifiEntries. + * + * These callbacks must be run on the MainThread. + * + * TODO (b/70983952): Investigate need for callbacks for saved/subscription entry updates. + */ + public interface WifiTrackerCallback { + /** + * Called when there are changes to + * {@link #getConnectedWifiEntry()} + * {@link #getWifiEntries()} + */ + @MainThread + void onWifiEntriesChanged(); + + /** + * Called when the state of Wi-Fi has changed. The new value can be read through + * {@link #getWifiState()} + */ + @MainThread + void onWifiStateChanged(); + } +} diff --git a/libs/WifiTrackerLib/tests/Android.bp b/libs/WifiTrackerLib/tests/Android.bp index e8f854893..97eefb7a2 100644 --- a/libs/WifiTrackerLib/tests/Android.bp +++ b/libs/WifiTrackerLib/tests/Android.bp @@ -19,9 +19,17 @@ android_test { static_libs: [ "android-support-test", "androidx.test.rules", + "frameworks-base-testutils", + "mockito-target-minus-junit4", + "truth-prebuilt", "WifiTrackerLib", ], + 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 39d6bf3f8..b92a764d7 100644 --- a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/ScanResultUpdaterTest.java @@ -16,16 +16,19 @@ package com.android.wifitrackerlib; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; import android.net.wifi.ScanResult; -import android.os.SystemClock; +import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import java.time.Clock; import java.util.Arrays; import java.util.List; @@ -33,6 +36,16 @@ public class ScanResultUpdaterTest { 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"; + private static final long NOW_MILLIS = 123_456_789; + + @Mock private Clock mMockClock; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mMockClock.millis()).thenReturn(NOW_MILLIS); + } private static ScanResult buildScanResult(String bssid, long timestampMs) { return new ScanResult( @@ -58,20 +71,17 @@ public class ScanResultUpdaterTest { BSSID_1, 20); // Add initial scan result. List should have 1 scan. - ScanResultUpdater sru = new ScanResultUpdater(); + ScanResultUpdater sru = new ScanResultUpdater(mMockClock); sru.update(Arrays.asList(oldResult)); - assertThat(sru.getScanResults().size(), equalTo(1)); - assertThat(sru.getScanResults().get(0), equalTo(oldResult)); + assertThat(sru.getScanResults()).containsExactly(oldResult); // Add new scan result. Old scan result should be replaced. sru.update(Arrays.asList(newResult)); - assertThat(sru.getScanResults().size(), equalTo(1)); - assertThat(sru.getScanResults().get(0), equalTo(newResult)); + assertThat(sru.getScanResults()).containsExactly(newResult); // Add old scan result back. New scan result should still remain. sru.update(Arrays.asList(oldResult)); - assertThat(sru.getScanResults().size(), equalTo(1)); - assertThat(sru.getScanResults().get(0), equalTo(newResult)); + assertThat(sru.getScanResults()).containsExactly(newResult); } /** @@ -79,21 +89,17 @@ public class ScanResultUpdaterTest { */ @Test public void testGetScanResults_filtersOldScans() { - long now = SystemClock.elapsedRealtime(); long maxScanAge = 15_000; - ScanResult oldResult = buildScanResult( - BSSID_1, now - (maxScanAge + 1)); - ScanResult newResult = buildScanResult( - BSSID_2, now); + ScanResult oldResult = buildScanResult(BSSID_1, NOW_MILLIS - (maxScanAge + 1)); + ScanResult newResult = buildScanResult(BSSID_2, NOW_MILLIS); // Add a new scan result and an out-of-date scan result. - ScanResultUpdater sru = new ScanResultUpdater(); + ScanResultUpdater sru = new ScanResultUpdater(mMockClock); sru.update(Arrays.asList(newResult, oldResult)); // New scan result should remain and out-of-date scan result should not be returned. - assertThat(sru.getScanResults(maxScanAge).size(), equalTo(1)); - assertThat(sru.getScanResults(maxScanAge).get(0), equalTo(newResult)); + assertThat(sru.getScanResults(maxScanAge)).containsExactly(newResult); } /** @@ -102,7 +108,7 @@ public class ScanResultUpdaterTest { */ @Test public void testGetScanResults_invalidMaxScanAgeMillis_throwsException() { - ScanResultUpdater sru = new ScanResultUpdater(15_000); + ScanResultUpdater sru = new ScanResultUpdater(mMockClock, 15_000); try { sru.getScanResults(20_000); fail("Should have thrown exception for maxScanAgeMillis too large."); @@ -116,19 +122,17 @@ public class ScanResultUpdaterTest { */ @Test public void testConstructor_maxScanAge_filtersOldScans() { - ScanResultUpdater sru = new ScanResultUpdater(15_000); - long now = SystemClock.elapsedRealtime(); + ScanResultUpdater sru = new ScanResultUpdater(mMockClock, 15_000); - ScanResult scan1 = buildScanResult(BSSID_1, now - 10_000); - ScanResult scan2 = buildScanResult(BSSID_2, now - 12_000); - ScanResult scan3 = buildScanResult(BSSID_3, now - 20_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); sru.update(Arrays.asList(scan1, scan2, scan3)); List scanResults = sru.getScanResults(); - assertThat(scanResults.size(), equalTo(2)); - assertThat(scanResults, contains(scan1, scan2)); + assertThat(scanResults).containsExactly(scan1, scan2); } /** @@ -137,23 +141,20 @@ public class ScanResultUpdaterTest { */ @Test public void testGetScanResults_overridesConstructorMaxScanAge() { - ScanResultUpdater sru = new ScanResultUpdater(15_000); - long now = SystemClock.elapsedRealtime(); + ScanResultUpdater sru = new ScanResultUpdater(mMockClock, 15_000); - ScanResult scan1 = buildScanResult(BSSID_1, now - 10_000); - ScanResult scan2 = buildScanResult(BSSID_2, now - 12_000); - ScanResult scan3 = buildScanResult(BSSID_3, now - 20_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); sru.update(Arrays.asList(scan1, scan2, scan3)); // Aged getScanResults should override the constructor max scan age. List scanResults = sru.getScanResults(11_000); - assertThat(scanResults.size(), equalTo(1)); - assertThat(scanResults, contains(scan1)); + assertThat(scanResults).containsExactly(scan1); // Non-aged getScanResults should revert to the constructor max scan age. scanResults = sru.getScanResults(); - assertThat(scanResults.size(), equalTo(2)); - assertThat(scanResults, contains(scan1, scan2)); + assertThat(scanResults).containsExactly(scan1, scan2); } } diff --git a/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java new file mode 100644 index 000000000..410f18277 --- /dev/null +++ b/libs/WifiTrackerLib/tests/src/com/android/wifitrackerlib/WifiTracker2Test.java @@ -0,0 +1,140 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkScoreManager; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.test.TestLooper; + +import androidx.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.Clock; + +public class WifiTracker2Test { + + @Mock private Lifecycle mMockLifecycle; + @Mock private Context mMockContext; + @Mock private WifiManager mMockWifiManager; + @Mock private ConnectivityManager mMockConnectivityManager; + @Mock private NetworkScoreManager mMockNetworkScoreManager; + @Mock private Clock mMockClock; + @Mock private WifiTracker2.WifiTrackerCallback mMockWifiTrackerCallback; + + private TestLooper mTestLooper = new TestLooper(); + + private final ArgumentCaptor mBroadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + private WifiTracker2 createTestWifiTracker2() { + final Handler testHandler = new Handler(mTestLooper.getLooper()); + + return new WifiTracker2(mMockLifecycle, mMockContext, + mMockWifiManager, + mMockConnectivityManager, + mMockNetworkScoreManager, + testHandler, + testHandler, + mMockClock, + 15_000, + 10_000, + mMockWifiTrackerCallback); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + /** + * Tests that receiving a wifi state change broadcast updates getWifiState(). + */ + @Test + public void testWifiStateChangeBroadcast_updatesWifiState() { + WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + 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); + + when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED); + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION)); + + assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_ENABLED); + + } + + /** + * Tests that receiving a wifi state change broadcast notifies the listener. + */ + @Test + public void testWifiStateChangeBroadcast_NotifiesListener() { + WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + wifiTracker2.onStart(); + verify(mMockContext).registerReceiver(mBroadcastReceiverCaptor.capture(), + any(), any(), any()); + + mBroadcastReceiverCaptor.getValue().onReceive(mMockContext, + new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION)); + mTestLooper.dispatchAll(); + + verify(mMockWifiTrackerCallback, atLeastOnce()).onWifiStateChanged(); + } + + /** + * Tests that the wifi state is set correctly after onStart, even if no broadcast was received. + */ + @Test + public void testOnStart_setsWifiState() { + WifiTracker2 wifiTracker2 = createTestWifiTracker2(); + when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_DISABLED); + wifiTracker2.onStart(); + mTestLooper.dispatchAll(); + + assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_DISABLED); + + wifiTracker2.onStop(); + when(mMockWifiManager.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED); + wifiTracker2.onStart(); + mTestLooper.dispatchAll(); + + assertThat(wifiTracker2.getWifiState()).isEqualTo(WifiManager.WIFI_STATE_ENABLED); + } +} -- cgit v1.2.3