[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);
+    }
+}