[RTT] CTS for the public Wi-Fi RTT APIs
Baseline set of tests for the public Wi-Fi RTT API.
Note: some of these tests require an IEEE 802.11mc capable AP.
Bug: 63446747
Test: tests pass
Change-Id: I176d9adbef15ce1c33b4572d5eb7e6cdf672f021
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 37bf323..0bfb650 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
diff --git a/tests/cts/net/src/android/net/wifi/rtt/cts/TestBase.java b/tests/cts/net/src/android/net/wifi/rtt/cts/TestBase.java
new file mode 100644
index 0000000..a601683
--- /dev/null
+++ b/tests/cts/net/src/android/net/wifi/rtt/cts/TestBase.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2018 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 android.net.wifi.rtt.cts;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.net.wifi.rtt.RangingResult;
+import android.net.wifi.rtt.RangingResultCallback;
+import android.net.wifi.rtt.WifiRttManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for Wi-Fi RTT CTS test cases. Provides a uniform configuration and event management
+ * facility.
+ */
+public class TestBase extends AndroidTestCase {
+ protected static final String TAG = "WifiRttCtsTests";
+
+ // The SSID of the test AP which supports IEEE 802.11mc
+ // TODO b/74518964: finalize correct method to refer to an AP in the test lab
+ static final String SSID_OF_TEST_AP = "standalone_rtt";
+
+ // wait for Wi-Fi RTT to become available
+ private static final int WAIT_FOR_RTT_CHANGE_SECS = 10;
+
+ // wait for Wi-Fi scan results to become available
+ private static final int WAIT_FOR_SCAN_RESULTS_SECS = 20;
+
+ protected WifiRttManager mWifiRttManager;
+ protected WifiManager mWifiManager;
+ private LocationManager mLocationManager;
+ private WifiManager.WifiLock mWifiLock;
+
+ private final HandlerThread mHandlerThread = new HandlerThread("SingleDeviceTest");
+ protected final Executor mExecutor;
+ {
+ mHandlerThread.start();
+ mExecutor = new HandlerExecutor(new Handler(mHandlerThread.getLooper()));
+ }
+
+ /**
+ * Returns a flag indicating whether or not Wi-Fi RTT should be tested. Wi-Fi RTT
+ * should be tested if the feature is supported on the current device.
+ */
+ static boolean shouldTestWifiRtt(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_WIFI_RTT);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ if (!shouldTestWifiRtt(getContext())) {
+ return;
+ }
+
+ mLocationManager = (LocationManager) getContext().getSystemService(
+ Context.LOCATION_SERVICE);
+ assertTrue("RTT testing requires Location to be enabled",
+ mLocationManager.isLocationEnabled());
+
+ mWifiRttManager = (WifiRttManager) getContext().getSystemService(
+ Context.WIFI_RTT_RANGING_SERVICE);
+ assertNotNull("Wi-Fi RTT Manager", mWifiRttManager);
+
+ mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
+ assertNotNull("Wi-Fi Manager", mWifiManager);
+ mWifiLock = mWifiManager.createWifiLock(TAG);
+ mWifiLock.acquire();
+ if (!mWifiManager.isWifiEnabled()) {
+ mWifiManager.setWifiEnabled(true);
+ }
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(WifiRttManager.ACTION_WIFI_RTT_STATE_CHANGED);
+ WifiRttBroadcastReceiver receiver = new WifiRttBroadcastReceiver();
+ mContext.registerReceiver(receiver, intentFilter);
+ if (!mWifiRttManager.isAvailable()) {
+ assertTrue("Timeout waiting for Wi-Fi RTT to change status",
+ receiver.waitForStateChange());
+ assertTrue("Wi-Fi RTT is not available (should be)", mWifiRttManager.isAvailable());
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (!shouldTestWifiRtt(getContext())) {
+ super.tearDown();
+ return;
+ }
+
+ super.tearDown();
+ }
+
+ class WifiRttBroadcastReceiver extends BroadcastReceiver {
+ private CountDownLatch mBlocker = new CountDownLatch(1);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (WifiRttManager.ACTION_WIFI_RTT_STATE_CHANGED.equals(intent.getAction())) {
+ mBlocker.countDown();
+ }
+ }
+
+ boolean waitForStateChange() throws InterruptedException {
+ return mBlocker.await(WAIT_FOR_RTT_CHANGE_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ class WifiScansBroadcastReceiver extends BroadcastReceiver {
+ private CountDownLatch mBlocker = new CountDownLatch(1);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())) {
+ mBlocker.countDown();
+ }
+ }
+
+ boolean waitForStateChange() throws InterruptedException {
+ return mBlocker.await(WAIT_FOR_SCAN_RESULTS_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ class ResultCallback extends RangingResultCallback {
+ private CountDownLatch mBlocker = new CountDownLatch(1);
+ private int mCode; // 0: success, otherwise RangingResultCallback STATUS_CODE_*.
+ private List<RangingResult> mResults;
+
+ @Override
+ public void onRangingFailure(int code) {
+ mCode = code;
+ mResults = null; // not necessary since intialized to null - but for completeness
+ mBlocker.countDown();
+ }
+
+ @Override
+ public void onRangingResults(List<RangingResult> results) {
+ mCode = 0; // not necessary since initialized to 0 - but for completeness
+ mResults = results;
+ mBlocker.countDown();
+ }
+
+ /**
+ * Waits for the listener callback to be called - or an error (timeout, interruption).
+ * Returns true on callback called, false on error (timeout, interruption).
+ */
+ boolean waitForCallback() throws InterruptedException {
+ return mBlocker.await(WAIT_FOR_RTT_CHANGE_SECS, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Returns the code of the callback operation. Will be 0 for success (onRangingResults
+ * called), else (if onRangingFailure called) will be one of the STATUS_CODE_* values.
+ */
+ int getCode() {
+ return mCode;
+ }
+
+ /**
+ * Returns the list of ranging results. In cases of error (getCode() != 0) will return null.
+ */
+ List<RangingResult> getResults() {
+ return mResults;
+ }
+ }
+
+ /**
+ * Start a scan and return a list of observed ScanResults (APs).
+ */
+ protected List<ScanResult> scanAps() throws InterruptedException {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ WifiScansBroadcastReceiver receiver = new WifiScansBroadcastReceiver();
+ mContext.registerReceiver(receiver, intentFilter);
+
+ mWifiManager.startScan();
+ receiver.waitForStateChange();
+ mContext.unregisterReceiver(receiver);
+ return mWifiManager.getScanResults();
+ }
+
+ /**
+ * Start a scan and return the test AP with the specified SSID and which supports IEEE 802.11mc.
+ * If the AP is not found re-attempts the scan maxScanRetries times (i.e. total number of
+ * scans can be maxScanRetries + 1).
+ *
+ * Returns null if test AP is not found in the specified number of scans.
+ *
+ * @param ssid The SSID of the test AP
+ * @param maxScanRetries Maximum number of scans retries (in addition to first scan).
+ */
+ protected ScanResult scanForTestAp(String ssid, int maxScanRetries)
+ throws InterruptedException {
+ int scanCount = 0;
+ while (scanCount <= maxScanRetries) {
+ for (ScanResult scanResult : scanAps()) {
+ if (!scanResult.is80211mcResponder()) {
+ continue;
+ }
+ if (!ssid.equals(scanResult.SSID)) {
+ continue;
+ }
+
+ return scanResult;
+ }
+
+ scanCount++;
+ }
+
+ return null;
+ }
+}
diff --git a/tests/cts/net/src/android/net/wifi/rtt/cts/WifiRttTest.java b/tests/cts/net/src/android/net/wifi/rtt/cts/WifiRttTest.java
new file mode 100644
index 0000000..0e6b306
--- /dev/null
+++ b/tests/cts/net/src/android/net/wifi/rtt/cts/WifiRttTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2018 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 android.net.wifi.rtt.cts;
+
+import android.content.IntentFilter;
+import android.net.wifi.ScanResult;
+import android.net.wifi.rtt.RangingRequest;
+import android.net.wifi.rtt.RangingResult;
+import android.net.wifi.rtt.WifiRttManager;
+
+import com.android.compatibility.common.util.DeviceReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Wi-Fi RTT CTS test: range to all available Access Points which support IEEE 802.11mc.
+ */
+public class WifiRttTest extends TestBase {
+ // Max number of scan retries to do while searching for APs supporting IEEE 802.11mc
+ private static final int MAX_NUM_SCAN_RETRIES_SEARCHING_FOR_IEEE80211MC_AP = 2;
+
+ // Number of RTT measurements per AP
+ private static final int NUM_OF_RTT_ITERATIONS = 10;
+
+ // Maximum failure rate of RTT measurements (percentage)
+ private static final int MAX_FAILURE_RATE_PERCENT = 10;
+
+ // Maximum variation from the average measurement (measures consistency)
+ private static final int MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM = 1000;
+
+ // Minimum valid RSSI value
+ private static final int MIN_VALID_RSSI = -100;
+
+ /**
+ * Test Wi-Fi RTT ranging operation:
+ * - Scan for visible APs for the test AP (which is validated to support IEEE 802.11mc)
+ * - Perform N (constant) RTT operations
+ * - Validate:
+ * - Failure ratio < threshold (constant)
+ * - Result margin < threshold (constant)
+ */
+ public void testRangingToTestAp() throws InterruptedException {
+ if (!shouldTestWifiRtt(getContext())) {
+ return;
+ }
+
+ // Scan for IEEE 802.11mc supporting APs
+ ScanResult testAp = scanForTestAp(SSID_OF_TEST_AP,
+ MAX_NUM_SCAN_RETRIES_SEARCHING_FOR_IEEE80211MC_AP);
+ assertTrue("Cannot find test AP", testAp != null);
+
+ // Perform RTT operations
+ RangingRequest request = new RangingRequest.Builder().addAccessPoint(testAp).build();
+ List<RangingResult> allResults = new ArrayList<>();
+ int numFailures = 0;
+ int distanceSum = 0;
+ int distanceMin = 0;
+ int distanceMax = 0;
+ int[] statuses = new int[NUM_OF_RTT_ITERATIONS];
+ int[] distanceMms = new int[NUM_OF_RTT_ITERATIONS];
+ int[] distanceStdDevMms = new int[NUM_OF_RTT_ITERATIONS];
+ int[] rssis = new int[NUM_OF_RTT_ITERATIONS];
+ for (int i = 0; i < NUM_OF_RTT_ITERATIONS; ++i) {
+ ResultCallback callback = new ResultCallback();
+ mWifiRttManager.startRanging(request, mExecutor, callback);
+ assertTrue("Wi-Fi RTT results: no callback on iteration " + i,
+ callback.waitForCallback());
+
+ List<RangingResult> currentResults = callback.getResults();
+ assertTrue("Wi-Fi RTT results: null results (onRangingFailure) on iteration " + i,
+ currentResults != null);
+ assertTrue("Wi-Fi RTT results: unexpected # of results (expect 1) on iteration " + i,
+ currentResults.size() == 1);
+ RangingResult result = currentResults.get(0);
+ assertTrue("Wi-Fi RTT results: invalid result (wrong BSSID) entry on iteration " + i,
+ result.getMacAddress().toString().equals(testAp.BSSID));
+
+ allResults.add(result);
+ int status = result.getStatus();
+ statuses[i] = status;
+ if (status == RangingResult.STATUS_SUCCESS) {
+ distanceSum += result.getDistanceMm();
+ if (i == 0) {
+ distanceMin = result.getDistanceMm();
+ distanceMax = result.getDistanceMm();
+ } else {
+ distanceMin = Math.min(distanceMin, result.getDistanceMm());
+ distanceMax = Math.max(distanceMax, result.getDistanceMm());
+ }
+
+ assertTrue("Wi-Fi RTT results: invalid RSSI on iteration " + i,
+ result.getRssi() >= MIN_VALID_RSSI);
+
+ distanceMms[i - numFailures] = result.getDistanceMm();
+ distanceStdDevMms[i - numFailures] = result.getDistanceStdDevMm();
+ rssis[i - numFailures] = result.getRssi();
+ } else {
+ numFailures++;
+ }
+ }
+
+ // Save results to log
+ int numGoodResults = NUM_OF_RTT_ITERATIONS - numFailures;
+ DeviceReportLog reportLog = new DeviceReportLog(TAG, "testRangingToTestAp");
+ reportLog.addValues("status_codes", statuses, ResultType.NEUTRAL, ResultUnit.NONE);
+ reportLog.addValues("distance_mm", Arrays.copyOf(distanceMms, numGoodResults),
+ ResultType.NEUTRAL, ResultUnit.NONE);
+ reportLog.addValues("distance_stddev_mm", Arrays.copyOf(distanceStdDevMms, numGoodResults),
+ ResultType.NEUTRAL, ResultUnit.NONE);
+ reportLog.addValues("rssi_dbm", Arrays.copyOf(rssis, numGoodResults), ResultType.NEUTRAL,
+ ResultUnit.NONE);
+ reportLog.submit();
+
+ // Analyze results
+ assertTrue("Wi-Fi RTT failure rate exceeds threshold",
+ numFailures <= NUM_OF_RTT_ITERATIONS * MAX_FAILURE_RATE_PERCENT / 100);
+ if (numFailures != NUM_OF_RTT_ITERATIONS) {
+ double distanceAvg = distanceSum / (NUM_OF_RTT_ITERATIONS - numFailures);
+ assertTrue("Wi-Fi RTT: Variation (max direction) exceeds threshold",
+ (distanceMax - distanceAvg) <= MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM);
+ assertTrue("Wi-Fi RTT: Variation (min direction) exceeds threshold",
+ (distanceAvg - distanceMin) <= MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM);
+ }
+ }
+
+ /**
+ * Validate that on Wi-Fi RTT availability change we get a broadcast + the API returns
+ * correct status.
+ */
+ public void testAvailabilityStatusChange() throws Exception {
+ if (!shouldTestWifiRtt(getContext())) {
+ return;
+ }
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(WifiRttManager.ACTION_WIFI_RTT_STATE_CHANGED);
+
+ // 1. Disable Wi-Fi
+ WifiRttBroadcastReceiver receiver1 = new WifiRttBroadcastReceiver();
+ mContext.registerReceiver(receiver1, intentFilter);
+ mWifiManager.setWifiEnabled(false);
+
+ assertTrue("Timeout waiting for Wi-Fi RTT to change status",
+ receiver1.waitForStateChange());
+ assertFalse("Wi-Fi RTT is available (should not be)", mWifiRttManager.isAvailable());
+
+ // 2. Enable Wi-Fi
+ WifiRttBroadcastReceiver receiver2 = new WifiRttBroadcastReceiver();
+ mContext.registerReceiver(receiver2, intentFilter);
+ mWifiManager.setWifiEnabled(true);
+
+ assertTrue("Timeout waiting for Wi-Fi RTT to change status",
+ receiver2.waitForStateChange());
+ assertTrue("Wi-Fi RTT is not available (should be)", mWifiRttManager.isAvailable());
+ }
+
+ /**
+ * Validate that when a request contains more range operations than allowed (by API) that we
+ * get an exception.
+ */
+ public void testRequestTooLarge() {
+ if (!shouldTestWifiRtt(getContext())) {
+ return;
+ }
+
+ RangingRequest.Builder builder = new RangingRequest.Builder();
+ for (int i = 0; i < RangingRequest.getMaxPeers() + 1; ++i) {
+ ScanResult dummy = new ScanResult();
+ dummy.BSSID = "00:01:02:03:04:05";
+ builder.addAccessPoint(dummy);
+ }
+
+ try {
+ mWifiRttManager.startRanging(builder.build(), mExecutor, new ResultCallback());
+ } catch (IllegalArgumentException e) {
+ return;
+ }
+
+ assertTrue(
+ "Did not receive expected IllegalArgumentException when tried to range to too "
+ + "many peers",
+ false);
+ }
+}