From 8ac4d00660bfd320ac2f954fb32075c3ac588021 Mon Sep 17 00:00:00 2001 From: ChenYu Date: Mon, 3 Feb 2025 02:30:26 +0000 Subject: WiFi Aware RTT Disable Test Cases test_disable_location test_disable_wifi Bug: 366011709 Test: https://paste.googleplex.com/6476905889660928 Merged-In: I1407518cc6cd2d893cf0b75b95f4b1f102b678f3 Change-Id: Ie34e90308b348d0c98fa53062a7b6818bc64f9f6 --- .../com.google.snippet.wifi/aware/Android.bp | 1 + .../aware/AndroidManifestNew.xml | 5 +- .../aware/WifiAwareJsonDeserializer.java | 1 - .../aware/WifiAwareManagerSnippet.java | 103 +++++- .../aware/WifiAwareSnippetConverter.java | 34 ++ .../multidevices/test/aware/integration/Android.bp | 19 ++ .../aware/integration/wifi_rtt_disable_test.py | 375 +++++++++++++++++++++ 7 files changed, 535 insertions(+), 3 deletions(-) create mode 100644 tests/hostsidetests/multidevices/test/aware/integration/wifi_rtt_disable_test.py diff --git a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/Android.bp b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/Android.bp index 09249b521d..c0edd5286f 100644 --- a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/Android.bp +++ b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/Android.bp @@ -53,6 +53,7 @@ android_test { "compatibility-device-util-axt", "guava", "mobly-snippet-lib", + "mobly-bundled-snippets-lib", ], min_sdk_version: "31", } diff --git a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/AndroidManifestNew.xml b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/AndroidManifestNew.xml index 7dc04dffee..cf13a13964 100644 --- a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/AndroidManifestNew.xml +++ b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/AndroidManifestNew.xml @@ -1,6 +1,7 @@ @@ -20,8 +21,10 @@ of a snippet class --> + com.google.snippet.wifi.aware.ConnectivityManagerSnippet, + com.google.android.mobly.snippet.bundled.WifiManagerSnippet"/> diff --git a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareJsonDeserializer.java b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareJsonDeserializer.java index 7c4eb8831e..70a0a639f6 100644 --- a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareJsonDeserializer.java +++ b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareJsonDeserializer.java @@ -96,7 +96,6 @@ public class WifiAwareJsonDeserializer { private static final String RANGING_REQUEST_PEER_IDS = "peer_ids"; private static final String RANGING_REQUEST_PEER_MACS = "peer_mac_addresses"; - private WifiAwareJsonDeserializer() { } diff --git a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareManagerSnippet.java b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareManagerSnippet.java index 146958c2c6..b96c1ea7c7 100644 --- a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareManagerSnippet.java +++ b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareManagerSnippet.java @@ -43,6 +43,7 @@ import android.net.wifi.rtt.RangingRequest; import android.net.wifi.rtt.RangingResult; import android.net.wifi.rtt.RangingResultCallback; import android.net.wifi.rtt.WifiRttManager; +import android.net.wifi.ScanResult; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Bundle; @@ -65,11 +66,15 @@ import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.rpc.RpcOptional; import com.google.android.mobly.snippet.util.Log; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.ListIterator; import java.util.concurrent.ConcurrentHashMap; /** @@ -347,6 +352,10 @@ public class WifiAwareManagerSnippet implements Snippet { /** * Checks if Wi-Fi RTT is available. */ + @Rpc(description = "Check if Wi-Fi Aware is available") + public Boolean wifiRttIsAvailable() { + return mWifiRttManager.isAvailable(); + } private void checkWifiRttAvailable() throws WifiAwareManagerSnippetException { if (!mWifiRttManager.isAvailable()) { throw new WifiAwareManagerSnippetException("WiFi RTT is not available now."); @@ -832,6 +841,98 @@ public class WifiAwareManagerSnippet implements Snippet { } + /** + * Converts a JSON representation of a ScanResult to an actual ScanResult object. Mirror of + * the code in + * {@link com.googlecode.android_scripting.jsonrpc.JsonBuilder#buildJsonScanResult(ScanResult)}. + * + * @param j JSON object representing a ScanResult. + * @return a ScanResult object + * @throws JSONException on any JSON errors + */ + public static ScanResult getScanResult(JSONObject j) throws JSONException { + if (j == null) { + return null; + } + + ScanResult scanResult = new ScanResult(); + + if (j.has("BSSID")) { + scanResult.BSSID = j.getString("BSSID"); + } + if (j.has("SSID")) { + scanResult.SSID = j.getString("SSID"); + } + if (j.has("frequency")) { + scanResult.frequency = j.getInt("frequency"); + } + if (j.has("level")) { + scanResult.level = j.getInt("level"); + } + if (j.has("capabilities")) { + scanResult.capabilities = j.getString("capabilities"); + } + if (j.has("timestamp")) { + scanResult.timestamp = j.getLong("timestamp"); + } + if (j.has("centerFreq0")) { + scanResult.centerFreq0 = j.getInt("centerFreq0"); + } + if (j.has("centerFreq1")) { + scanResult.centerFreq1 = j.getInt("centerFreq1"); + } + if (j.has("channelWidth")) { + scanResult.channelWidth = j.getInt("channelWidth"); + } + if (j.has("operatorFriendlyName")) { + scanResult.operatorFriendlyName = j.getString("operatorFriendlyName"); + } + if (j.has("venueName")) { + scanResult.venueName = j.getString("venueName"); + } + + return scanResult; + } + + /** + * Converts a JSONArray toa a list of ScanResult. + * + * @param j JSONArray representing a collection of ScanResult objects + * @return a list of ScanResult objects + * @throws JSONException on any JSON error + */ + public static List getScanResults(JSONArray j) throws JSONException { + if (j == null || j.length() == 0) { + return null; + } + + ArrayList scanResults = new ArrayList<>(j.length()); + for (int i = 0; i < j.length(); ++i) { + scanResults.add(getScanResult(j.getJSONObject(i))); + } + + return scanResults; + } + + /** + * Starts Wi-Fi RTT ranging with Wi-Fi Aware access points. + * + * @param callbackId Assigned automatically by mobly for all async RPCs. + * @param requestJsonObject The ranging request in JSONArray type for calling {@link + * android.net.wifi.ScanResult}. + */ + @AsyncRpc(description = "Start ranging to an Access Points.") + public void wifiRttStartRangingToAccessPoints( + String callbackId, JSONArray requestJsonObject + ) throws JSONException, WifiAwareManagerSnippetException { + Log.v("wifiRttStartRangingToAccessPoints: " + requestJsonObject); + RangingRequest request = new RangingRequest.Builder().addAccessPoints( + getScanResults(requestJsonObject)).build(); + Log.v("Starting Wi-Fi RTT ranging with access point: " + request.toString()); + RangingCallback rangingCb = new RangingCallback(eventCache, callbackId); + mWifiRttManager.startRanging(request, command -> mHandler.post(command), rangingCb); + } + /** * Starts Wi-Fi RTT ranging with Wi-Fi Aware peers. * @@ -869,7 +970,7 @@ public class WifiAwareManagerSnippet implements Snippet { public void onRangingFailure(int code) { SnippetEvent event = new SnippetEvent(mCallbackId, EVENT_NAME_RANGING_RESULT); event.getData().putString("callbackName", "onRangingFailure"); - event.getData().putInt("statusCode", code); + event.getData().putInt("status", code); mEventCache.postEvent(event); } diff --git a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareSnippetConverter.java b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareSnippetConverter.java index 154d0eb8d2..edcfeac0ca 100644 --- a/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareSnippetConverter.java +++ b/tests/hostsidetests/multidevices/com.google.snippet.wifi/aware/WifiAwareSnippetConverter.java @@ -19,14 +19,17 @@ package com.google.snippet.wifi.aware; import android.net.NetworkRequest; import android.net.wifi.aware.PublishConfig; import android.net.wifi.aware.SubscribeConfig; +import android.net.wifi.ScanResult; import android.net.wifi.aware.WifiAwareNetworkSpecifier; import com.google.android.mobly.snippet.SnippetObjectConverter; +import com.google.android.mobly.snippet.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Type; +import java.util.List; /** * The converter class that allows users to use custom type as snippet RPC arguments and return @@ -34,6 +37,19 @@ import java.lang.reflect.Type; */ public class WifiAwareSnippetConverter implements SnippetObjectConverter { + + public static String trimQuotationMarks(String originalString) { + String result = originalString; + if (originalString == null) + return result; + if (originalString.length() > 2 + && originalString.charAt(0) == '"' + && originalString.charAt(originalString.length() - 1) == '"') { + result = originalString.substring(1, originalString.length() - 1); + } + return result; + } + @Override public JSONObject serialize(Object object) throws JSONException { // If the RPC method requires a custom return type, e.g. SubscribeConfig, PublishConfig, we @@ -49,6 +65,24 @@ public class WifiAwareSnippetConverter implements SnippetObjectConverter { return null; } + public static JSONObject serializeScanResult(ScanResult data) throws JSONException { + JSONObject result = new JSONObject(); + result.put("BSSID", data.BSSID); + result.put("SSID", trimQuotationMarks(data.getWifiSsid().toString())); + result.put("capabilities", data.capabilities); + result.put("centerFreq0", data.centerFreq0); + result.put("centerFreq1", data.centerFreq1); + result.put("channelWidth", data.channelWidth); + result.put("frequency", data.frequency); + result.put("level", data.level); + result.put("operatorFriendlyName", + (data.operatorFriendlyName != null) ? data.operatorFriendlyName.toString() : ""); + result.put("timestamp", data.timestamp); + result.put("venueName", (data.venueName != null) ? data.venueName.toString() : ""); + result.put("scan_result_parcel", SerializationUtil.parcelableToString(data)); + return result; + } + @Override public Object deserialize(JSONObject jsonObject, Type type) throws JSONException { // The parameters of Mobly RPC directly reference the Object type. diff --git a/tests/hostsidetests/multidevices/test/aware/integration/Android.bp b/tests/hostsidetests/multidevices/test/aware/integration/Android.bp index fa76467f0c..df52681d05 100644 --- a/tests/hostsidetests/multidevices/test/aware/integration/Android.bp +++ b/tests/hostsidetests/multidevices/test/aware/integration/Android.bp @@ -206,3 +206,22 @@ python_test_host { tags: ["mobly"], }, } + +python_test_host { + name: "WifiRttDisableTestCases", + main: "wifi_rtt_disable_test.py", + srcs: ["wifi_rtt_disable_test.py"], + device_common_data: [":wifi_aware_snippet_new"], + libs: [ + "aware_lib_utils", + "mobly", + "wifi_aware_constants", + ], + test_suites: [ + "general-tests", + ], + test_options: { + unit_test: false, + tags: ["mobly"], + }, +} diff --git a/tests/hostsidetests/multidevices/test/aware/integration/wifi_rtt_disable_test.py b/tests/hostsidetests/multidevices/test/aware/integration/wifi_rtt_disable_test.py new file mode 100644 index 0000000000..d87de30410 --- /dev/null +++ b/tests/hostsidetests/multidevices/test/aware/integration/wifi_rtt_disable_test.py @@ -0,0 +1,375 @@ +# Copyright (C) 2024 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. + +# Lint as: python3 +"""Wi-Fi Aware Rtt Disable test reimplemented in Mobly.""" +import functools +import logging +import signal +import sys +import time + +from aware import aware_lib_utils as autils +from aware import constants +from mobly import asserts +from mobly import base_test +from mobly import records +from mobly import test_runner +from mobly import utils +from mobly.controllers import android_device +from mobly.snippet import errors + +RUNTIME_PERMISSIONS = ( + 'android.permission.ACCESS_FINE_LOCATION', + 'android.permission.ACCESS_COARSE_LOCATION', + 'android.permission.NEARBY_WIFI_DEVICES', +) +PACKAGE_NAME = constants.WIFI_AWARE_SNIPPET_PACKAGE_NAME + +# Alias variable. +_DEFAULT_TIMEOUT = constants.WAIT_WIFI_STATE_TIME_OUT.total_seconds() +_CALLBACK_NAME = constants.DiscoverySessionCallbackParamsType.CALLBACK_NAME + +###################################################### +# status codes +###################################################### +_RANGING_FAIL_CODE_GENERIC = 1 +_RANGING_FAIL_CODE_RTT_NOT_AVAILABLE = 2 + + +# Timeout decorator block +class TimeoutError(Exception): + """Exception for timeout decorator related errors.""" + + +def _timeout_handler(): + """Handler function used by signal to terminate a timed out function.""" + raise TimeoutError() + + +def timeout(sec): + """A decorator used to add time out check to a function. + + This only works in main thread due to its dependency on signal module. + Do NOT use it if the decorated function does not run in the Main thread. + + Args: + sec: Number of seconds to wait before the function times out. No timeout + if set to 0 + + Returns: + What the decorated function returns. + + Raises: + TimeoutError is raised when time out happens. + """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if sec: + signal.signal(signal.SIGALRM, _timeout_handler) + signal.alarm(sec) + try: + return func(*args, **kwargs) + except TimeoutError as exc: + raise TimeoutError( + ('Function {} timed out after {} seconds.').format( + func.__name__, sec + ) + ) from exc + finally: + signal.alarm(0) + + return wrapper + + return decorator + + +class RttDisableTest(base_test.BaseTestClass): + """Test class for RTT ranging enable/disable flows.""" + + MODE_DISABLE_WIFI = 0 + MODE_DISABLE_LOCATIONING = 1 + + ads: list[android_device.AndroidDevice] + + def setup_class(self): + self.ads = self.register_controller(android_device, min_number=1) + + def setup_device(device: android_device.AndroidDevice): + autils.control_wifi(device, True) + device.load_snippet('wifi_aware_snippet', PACKAGE_NAME) + for permission in RUNTIME_PERMISSIONS: + device.adb.shell(['pm', 'grant', PACKAGE_NAME, permission]) + asserts.abort_all_if( + not device.wifi_aware_snippet.wifiAwareIsAvailable(), + f'{device} Wi-Fi Aware is not available.', + ) + + # Set up devices in parallel. + utils.concurrent_exec( + setup_device, + param_list=[[ad] for ad in self.ads], + max_workers=1, + raise_on_exception=True, + ) + + def setup_test(self): + for ad in self.ads: + autils.control_wifi(ad, True) + self.set_location_service(ad, True) + aware_avail = ad.wifi_aware_snippet.wifiAwareIsAvailable() + if not aware_avail: + ad.log.info('Aware not available. Waiting ...') + state_handler = ad.wifi_aware_snippet.wifiAwareMonitorStateChange() + state_handler.waitAndGet( + constants.WifiAwareBroadcast.WIFI_AWARE_AVAILABLE + ) + + def teardown_test(self): + utils.concurrent_exec( + self._teardown_test_on_device, + param_list=[[ad] for ad in self.ads], + max_workers=1, + raise_on_exception=True, + ) + utils.concurrent_exec( + lambda d: d.services.create_output_excerpts_all(self.current_test_info), + param_list=[[ad] for ad in self.ads], + raise_on_exception=True, + ) + + def _teardown_test_on_device(self, ad: android_device.AndroidDevice) -> None: + ad.wifi_aware_snippet.wifiAwareCloseAllWifiAwareSession() + ad.wifi_aware_snippet.wifiAwareMonitorStopStateChange() + # autils.control_wifi(ad, True) + + def on_fail(self, record: records.TestResult) -> None: + android_device.take_bug_reports( + self.ads, destination=self.current_test_info.output_path + ) + + def scan_networks( + self, dut: android_device.AndroidDevice, max_tries: int = 3 + ) -> list[dict[str, str]]: + """Perform a scan and return scan results. + + Args: + dut: Device under test. + max_tries: Retry scan to ensure network is found + + Returns: + an array of scan results. + """ + scan_results = [] + for _ in range(max_tries): + scan_results = dut.wifi_aware_snippet.wifiScanAndGetResults() + if scan_results: + break + + return scan_results + + def select_best_scan_results( + self, + scans: list[dict[str, str]], + select_count: int, + lowest_rssi: int = -80, + ): + """Select best result based on RSSI. + + Select the strongest 'select_count' scans in the input list based on + highest RSSI. Exclude all very weak signals, even if results in a shorter + list. + + Args: + scans: List of scan results. + select_count: An integer specifying how many scans to return at most. + lowest_rssi: The lowest RSSI to accept into the output. + + Returns: + a list of the strongest 'select_count' scan results from the scans + list. + """ + + def _take_rssi(element): + return element['level'] + + result = [] + scans.sort(key=_take_rssi, reverse=True) + for scan in scans: + logging.info( + 'scan type: %s, %s, %s', scan['SSID'], scan['level'], scan['BSSID'] + ) + if len(result) == select_count: + break + if scan['level'] < lowest_rssi: + break # rest are lower since we're sorted + result.append(scan) + + return result + + def set_location_service(self, ad, new_state): + """Set Location service on/off in Settings->Location. + + Args: + ad: android device object. + new_state: new state for "Location service". + If new_state is False, turn off location service. + If new_state if True, set location service to "High accuracy". + """ + ad.adb.shell('content insert --uri ' + ' content://com.google.settings/partner --bind ' + 'name:s:network_location_opt_in --bind value:s:1') + ad.adb.shell('content insert --uri ' + ' content://com.google.settings/partner --bind ' + 'name:s:use_location_for_services --bind value:s:1') + if new_state: + ad.adb.shell('settings put secure location_mode 3') + else: + ad.adb.shell('settings put secure location_mode 0') + + def force_airplane_mode(self, ad, new_state, timeout_value=60): + """Force the device to set airplane mode on or off by adb shell command. + + Args: + ad: android device object. + new_state: Turn on airplane mode if True. + Turn off airplane mode if False. + timeout_value: max wait time for 'adb wait-for-device' + + Returns: + True if success. + False if timeout. + """ + + # Using timeout decorator. + # Wait for device with timeout. If after seconds, adb + # is still waiting for device, throw TimeoutError exception. + @timeout(timeout_value) + def wait_for_device_with_timeout(ad): + ad.adb.wait_for_device() + + try: + wait_for_device_with_timeout(ad) + ad.adb.shell('settings put global airplane_mode_on {}'.format( + 1 if new_state else 0)) + ad.adb.shell('am broadcast -a android.intent.action.AIRPLANE_MODE') + except TimeoutError: + # adb wait for device timeout + return False + return True + + def run_disable_rtt(self, disable_mode): + """Validate the RTT ranging feature if RTT disabled. + + Validate the RTT disabled flows: whether by disabling Wi-Fi or entering + doze mode. + + Args: + disable_mode: The particular mechanism in which RTT is disabled. One of + the MODE_* constants. + """ + dut = self.ads[0] + + # validate start-up conditions + asserts.assert_true( + dut.wifi_aware_snippet.wifiRttIsAvailable(), 'RTT is not available' + ) + + # scan to get some APs to be used later + all_aps = self.select_best_scan_results( + self.scan_networks(dut), select_count=1 + ) + asserts.assert_true(all_aps, 'Need at least one visible AP!') + + # disable RTT and validate broadcast & API + if disable_mode == self.MODE_DISABLE_WIFI: + # disabling Wi-Fi is not sufficient: since scan mode (and hence RTT) will + # remain enabled - we need to disable the Wi-Fi chip aka Airplane Mode + asserts.assert_true( + self.force_airplane_mode(dut, True), + 'Can not turn on airplane mode on: %s' % dut.serial, + ) + autils.control_wifi(dut, False) + elif disable_mode == self.MODE_DISABLE_LOCATIONING: + self.set_location_service(dut, False) + time.sleep(10) + dut.log.info( + 'WiFi RTT status: %s', dut.wifi_aware_snippet.wifiRttIsAvailable() + ) + asserts.assert_false( + dut.wifi_aware_snippet.wifiRttIsAvailable(), 'RTT is available' + ) + + # request a range and validate error + dut.log.info('access points input: %s', all_aps[0:1]) + ranging_cb_handler = ( + dut.wifi_aware_snippet.wifiRttStartRangingToAccessPoints(all_aps[0:1]) + ) + event = ranging_cb_handler.waitAndGet( + event_name=constants.RangingResultCb.EVENT_NAME_ON_RANGING_RESULT, + timeout=_DEFAULT_TIMEOUT, + ) + + callback_name = event.data.get( + constants.RangingResultCb.DATA_KEY_CALLBACK_NAME, None + ) + dut.log.info('StartRangingToAccessPoints callback = %s', callback_name) + asserts.assert_equal( + callback_name, + constants.RangingResultCb.CB_METHOD_ON_RANGING_FAILURE, + 'Should be ranging failed.', + ) + status_code = event.data.get( + constants.RangingResultCb.DATA_KEY_RESULT_STATUS, None + ) + dut.log.info('StartRangingToAccessPoints status code = %s', status_code) + asserts.assert_equal( + status_code, _RANGING_FAIL_CODE_RTT_NOT_AVAILABLE, 'Invalid error code' + ) + + # enable RTT and validate broadcast & API + if disable_mode == self.MODE_DISABLE_WIFI: + asserts.assert_true( + self.force_airplane_mode(dut, False), + 'Can not turn off airplane mode on: %s' % dut.serial, + ) + autils.control_wifi(dut, True) + elif disable_mode == self.MODE_DISABLE_LOCATIONING: + self.set_location_service(dut, True) + + asserts.assert_true( + dut.wifi_aware_snippet.wifiRttIsAvailable(), 'RTT is not available' + ) + + def test_disable_wifi(self): + """Validate that getting expected broadcast when Wi-Fi is disabled and that any range requests are rejected. + """ + self.run_disable_rtt(self.MODE_DISABLE_WIFI) + + def test_disable_location(self): + """Validate that getting expected broadcast when locationing is disabled and that any range requests are rejected. + """ + self.run_disable_rtt(self.MODE_DISABLE_LOCATIONING) + + +if __name__ == '__main__': + # Take test args + if '--' in sys.argv: + index = sys.argv.index('--') + sys.argv = sys.argv[:1] + sys.argv[index + 1 :] + + test_runner.main() -- cgit v1.2.3-59-g8ed1b