From 439c05ce8bf66ae4f6f34c786084dd95c032a362 Mon Sep 17 00:00:00 2001 From: Etan Cohen Date: Wed, 28 Feb 2018 16:25:22 -0800 Subject: [PATCH] [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 --- tests/cts/net/AndroidManifest.xml | 1 + .../android/net/wifi/rtt/cts/TestBase.java | 242 ++++++++++++++++++ .../android/net/wifi/rtt/cts/WifiRttTest.java | 202 +++++++++++++++ 3 files changed, 445 insertions(+) create mode 100644 tests/cts/net/src/android/net/wifi/rtt/cts/TestBase.java create mode 100644 tests/cts/net/src/android/net/wifi/rtt/cts/WifiRttTest.java diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml index 37bf323486..0bfb650882 100644 --- a/tests/cts/net/AndroidManifest.xml +++ b/tests/cts/net/AndroidManifest.xml @@ -21,6 +21,7 @@ + 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 0000000000..a601683a7c --- /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 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 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 getResults() { + return mResults; + } + } + + /** + * Start a scan and return a list of observed ScanResults (APs). + */ + protected List 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 0000000000..0e6b306cb0 --- /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 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 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); + } +}