Add CtsConnectivityMultiDevicesTestCases for hotspot
This test is conducting two physical devices: a device
acting as the hotspot and a separate client device. The test
assesses whether the client can connect to the hotspot and utilize
its internet connection.
This test verifies the basic functionality of the hotspot feature
by exercising the entire system without mocking any component.
This is crucial because various system components, like cellular,
Wi-Fi, routing rules, tethering, and hardware offload, can have
regressions or design changes that impact the basic functionality.
Test: m connectivity_multi_devices_snippet && \
atest CtsConnectivityMultiDevicesTestCases
(2 physical devices near each other are needed
to perform this test)
Bug: N/A
Change-Id: Ib97495c48f313889cedae03d59762482da10cfd8
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
new file mode 100644
index 0000000..5940cbb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -0,0 +1,37 @@
+// 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.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "connectivity_multi_devices_snippet",
+ defaults: [
+ "ConnectivityTestsLatestSdkDefaults",
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
+ srcs: [
+ "ConnectivityMultiDevicesSnippet.kt",
+ ],
+ manifest: "AndroidManifest.xml",
+ static_libs: [
+ "androidx.test.runner",
+ "mobly-snippet-lib",
+ "cts-net-utils",
+ ],
+ platform_apis: true,
+ min_sdk_version: "30", // R
+}
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..9ed8146
--- /dev/null
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.snippet.connectivity">
+ <!-- Declare the minimum Android SDK version and internet permission,
+ which are required by Mobly Snippet Lib since it uses network socket. -->
+ <uses-sdk android:minSdkVersion="30" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application>
+ <!-- Add any classes that implement the Snippet interface as meta-data, whose
+ value is a comma-separated string, each section being the package path
+ of a snippet class -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+ </application>
+ <!-- Add an instrumentation tag so that the app can be launched through an
+ instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
+ is derived from `AndroidJUnitRunner`, and is required to use the
+ Mobly Snippet Lib. -->
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.snippet.connectivity" />
+</manifest>
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
new file mode 100644
index 0000000..115210b
--- /dev/null
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+package com.google.snippet.connectivity
+
+import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.cts.util.CtsNetUtils
+import android.net.cts.util.CtsTetheringUtils
+import android.net.wifi.ScanResult
+import android.net.wifi.SoftApConfiguration
+import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiNetworkSpecifier
+import android.net.wifi.WifiSsid
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+
+class ConnectivityMultiDevicesSnippet : Snippet {
+ private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ private val wifiManager = context.getSystemService(WifiManager::class.java)!!
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val pm = context.packageManager
+ private val ctsNetUtils = CtsNetUtils(context)
+ private val ctsTetheringUtils = CtsTetheringUtils(context)
+ private var oldSoftApConfig: SoftApConfiguration? = null
+
+ @Rpc(description = "Check whether the device has wifi feature.")
+ fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
+
+ @Rpc(description = "Check whether the device has telephony feature.")
+ fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
+
+ @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
+ fun isStaApConcurrencySupported() {
+ wifiManager.isStaApConcurrencySupported()
+ }
+
+ @Rpc(description = "Request cellular connection and ensure it is the default network.")
+ fun requestCellularAndEnsureDefault() {
+ ctsNetUtils.disableWifi()
+ val network = ctsNetUtils.connectToCell()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Unrequest cellular connection.")
+ fun unrequestCellular() {
+ ctsNetUtils.disconnectFromCell()
+ }
+
+ @Rpc(description = "Ensure any wifi is connected and is the default network.")
+ fun ensureWifiIsDefault() {
+ val network = ctsNetUtils.ensureWifiConnected()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Connect to specified wifi network.")
+ // Suppress warning because WifiManager methods to connect to a config are
+ // documented not to be deprecated for privileged users.
+ @Suppress("DEPRECATION")
+ fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
+ val specifier = WifiNetworkSpecifier.Builder()
+ .setSsid(ssid)
+ .setWpa2Passphrase(passphrase)
+ .setBand(ScanResult.WIFI_BAND_24_GHZ)
+ .build()
+ val wifiConfig = WifiConfiguration()
+ wifiConfig.SSID = "\"" + ssid + "\""
+ wifiConfig.preSharedKey = "\"" + passphrase + "\""
+ wifiConfig.hiddenSSID = true
+ wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
+
+ // Register network callback for the specific wifi.
+ val networkCallback = TestableNetworkCallback()
+ val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
+ .setNetworkSpecifier(specifier)
+ .build()
+ cm.registerNetworkCallback(wifiRequest, networkCallback)
+
+ try {
+ // Add the test configuration and connect to it.
+ val connectUtil = ConnectUtil(context)
+ connectUtil.connectToWifiConfig(wifiConfig)
+
+ val event = networkCallback.expect<Available>()
+ if (requireValidation) {
+ networkCallback.eventuallyExpect<CapabilitiesChanged> {
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ }
+ return event.network
+ } finally {
+ cm.unregisterNetworkCallback(networkCallback)
+ }
+ }
+
+ @Rpc(description = "Check whether the device supports hotspot feature.")
+ fun hasHotspotFeature(): Boolean {
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ return tetheringCallback.isWifiTetheringSupported(context)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Start a hotspot with given SSID and passphrase.")
+ fun startHotspot(ssid: String, passphrase: String) {
+ // Store old config.
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ oldSoftApConfig = wifiManager.getSoftApConfiguration()
+ }
+
+ val softApConfig = SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.toByteArray()))
+ .setPassphrase(passphrase, SECURITY_TYPE_WPA2_PSK)
+ .setBand(SoftApConfiguration.BAND_2GHZ)
+ .build()
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(softApConfig)
+ }
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ tetheringCallback.expectNoTetheringActive()
+ ctsTetheringUtils.startWifiTethering(tetheringCallback)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Stop all tethering.")
+ fun stopAllTethering() {
+ ctsTetheringUtils.stopAllTethering()
+
+ // Restore old config.
+ oldSoftApConfig?.let {
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(it)
+ }
+ }
+ }
+}