1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.testutils
18
19 import android.Manifest.permission
20 import android.content.BroadcastReceiver
21 import android.content.Context
22 import android.content.Intent
23 import android.content.IntentFilter
24 import android.net.ConnectivityManager
25 import android.net.Network
26 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
27 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
28 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
29 import android.net.NetworkCapabilities.TRANSPORT_WIFI
30 import android.net.NetworkRequest
31 import android.net.wifi.ScanResult
32 import android.net.wifi.WifiConfiguration
33 import android.net.wifi.WifiManager
34 import android.os.ParcelFileDescriptor
35 import android.os.SystemClock
36 import android.util.Log
37 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
38 import com.android.testutils.RecorderCallback.CallbackEntry
39 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
40 import java.util.concurrent.CompletableFuture
41 import java.util.concurrent.TimeUnit
42 import kotlin.test.assertNotNull
43 import kotlin.test.assertTrue
44 import kotlin.test.fail
45
46 private const val MAX_WIFI_CONNECT_RETRIES = 10
47 private const val WIFI_CONNECT_INTERVAL_MS = 500L
48 private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
49
50 // Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
51 // the error code constants are not (b/204277752)
52 private const val WIFI_ERROR_IN_PROGRESS = 1
53 private const val WIFI_ERROR_BUSY = 2
54
55 class ConnectUtil(private val context: Context) {
56 companion object {
57 @JvmStatic
58 val VIRTUAL_SSIDS = listOf("VirtWifi", "AndroidWifi")
59 }
60 private val TAG = ConnectUtil::class.java.simpleName
61
62 private val cm = context.getSystemService(ConnectivityManager::class.java)
63 ?: fail("Could not find ConnectivityManager")
64 private val wifiManager = context.getSystemService(WifiManager::class.java)
65 ?: fail("Could not find WifiManager")
66
ensureWifiConnectednull67 fun ensureWifiConnected(): Network = ensureWifiConnected(requireValidated = false)
68 fun ensureWifiValidated(): Network = ensureWifiConnected(requireValidated = true)
69
70 fun ensureCellularValidated(): Network {
71 val cb = TestableNetworkCallback()
72 cm.requestNetwork(
73 NetworkRequest.Builder()
74 .addTransportType(TRANSPORT_CELLULAR)
75 .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
76 return tryTest {
77 val errorMsg = "The device does not have mobile data available. Check that it is " +
78 "setup with a SIM card that has a working data plan, that the APN " +
79 "configuration is valid, and that the device can access the internet through " +
80 "mobile data."
81 cb.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
82 it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
83 }.network
84 } cleanup {
85 cm.unregisterNetworkCallback(cb)
86 }
87 }
88
ensureWifiConnectednull89 private fun ensureWifiConnected(requireValidated: Boolean): Network {
90 val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
91 cm.registerNetworkCallback(NetworkRequest.Builder()
92 .addTransportType(TRANSPORT_WIFI)
93 .addCapability(NET_CAPABILITY_INTERNET)
94 .build(), callback)
95
96 return tryTest {
97 val connInfo = wifiManager.connectionInfo
98 Log.d(TAG, "connInfo=" + connInfo)
99 if (connInfo == null || connInfo.networkId == -1) {
100 clearWifiBlocklist()
101 val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
102 // Read the output stream to ensure the command has completed
103 ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
104 val config = getOrCreateWifiConfiguration()
105 connectToWifiConfig(config)
106 }
107 val errorMsg = if (requireValidated) {
108 "The wifi access point did not have access to the internet after " +
109 "$WIFI_CONNECT_TIMEOUT_MS ms. Check that it has a working connection."
110 } else {
111 "Could not connect to a wifi access point within $WIFI_CONNECT_TIMEOUT_MS ms. " +
112 "Check that the test device has a wifi network configured, and that the " +
113 "test access point is functioning properly."
114 }
115 val cb = callback.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
116 (!requireValidated || it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
117 }
118 cb.network
119 } cleanup {
120 cm.unregisterNetworkCallback(callback)
121 }
122 }
123
124 // Suppress warning because WifiManager methods to connect to a config are
125 // documented not to be deprecated for privileged users.
126 @Suppress("DEPRECATION")
connectToWifiConfignull127 fun connectToWifiConfig(config: WifiConfiguration) {
128 repeat(MAX_WIFI_CONNECT_RETRIES) {
129 val error = runAsShell(permission.NETWORK_SETTINGS) {
130 val listener = ConnectWifiListener()
131 wifiManager.connect(config, listener)
132 listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
133 } ?: return // Connect succeeded
134
135 // Only retry for IN_PROGRESS and BUSY
136 if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
137 fail("Failed to connect to " + config.SSID + ": " + error)
138 }
139 Log.w(TAG, "connect failed with $error; waiting before retry")
140 SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
141 }
142 fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
143 }
144
145 private class ConnectWifiListener : WifiManager.ActionListener {
146 /**
147 * Future completed when the connect process ends. Provides the error code or null if none.
148 */
149 val connectFuture = CompletableFuture<Int?>()
onSuccessnull150 override fun onSuccess() {
151 connectFuture.complete(null)
152 }
153
onFailurenull154 override fun onFailure(reason: Int) {
155 connectFuture.complete(reason)
156 }
157 }
158
getOrCreateWifiConfigurationnull159 private fun getOrCreateWifiConfiguration(): WifiConfiguration {
160 val configs = runAsShell(permission.NETWORK_SETTINGS) {
161 wifiManager.getConfiguredNetworks()
162 }
163 // If no network is configured, add a config for virtual access points if applicable
164 if (configs.size == 0) {
165 val scanResults = getWifiScanResults()
166 val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
167 assertNotNull(virtualConfig, "The device has no configured wifi network")
168 return virtualConfig
169 }
170 // No need to add a configuration: there is already one.
171 if (configs.size > 1) {
172 // For convenience in case of local testing on devices with multiple saved configs,
173 // prefer the first configuration that is in range.
174 // In actual tests, there should only be one configuration, and it should be usable as
175 // assumed by WifiManagerTest.testConnect.
176 Log.w(TAG, "Multiple wifi configurations found: " +
177 configs.joinToString(", ") { it.SSID })
178 val scanResultsList = getWifiScanResults()
179 Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
180 "${it.SSID} (${it.level})"
181 })
182
183 val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
184 return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
185 }
186 return configs[0]
187 }
188
getWifiScanResultsnull189 private fun getWifiScanResults(): List<ScanResult> {
190 val scanResultsFuture = CompletableFuture<List<ScanResult>>()
191 runAsShell(permission.NETWORK_SETTINGS) {
192 val receiver: BroadcastReceiver = object : BroadcastReceiver() {
193 override fun onReceive(context: Context, intent: Intent) {
194 scanResultsFuture.complete(wifiManager.scanResults)
195 }
196 }
197 context.registerReceiver(receiver,
198 IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
199 wifiManager.startScan()
200 }
201 return try {
202 scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
203 } catch (e: Exception) {
204 throw AssertionError("Wifi scan results not received within timeout", e)
205 }
206 }
207
208 /**
209 * If a virtual wifi network is detected, add a configuration for that network.
210 * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
211 */
maybeConfigureVirtualNetworknull212 private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
213 // Virtual wifi networks used on the emulator and cloud testing infrastructure
214 Log.d(TAG, "Wifi scan results: $scanResults")
215 val virtualScanResult = scanResults.firstOrNull { VIRTUAL_SSIDS.contains(it.SSID) }
216 ?: return null
217
218 // Only add the virtual configuration if the virtual AP is detected in scans
219 val virtualConfig = WifiConfiguration()
220 // ASCII SSIDs need to be surrounded by double quotes
221 virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
222 virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
223 runAsShell(permission.NETWORK_SETTINGS) {
224 val networkId = wifiManager.addNetwork(virtualConfig)
225 assertTrue(networkId >= 0)
226 assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
227 }
228 return virtualConfig
229 }
230
231 /**
232 * Re-enable wifi networks that were blocked, typically because no internet connection was
233 * detected the last time they were connected. This is necessary to make sure wifi can reconnect
234 * to them.
235 */
clearWifiBlocklistnull236 private fun clearWifiBlocklist() {
237 runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
238 for (cfg in wifiManager.configuredNetworks) {
239 assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
240 }
241 }
242 }
243 }
244
eventuallyExpectnull245 private inline fun <reified T : CallbackEntry> TestableNetworkCallback.eventuallyExpect(
246 errorMsg: String,
247 crossinline predicate: (T) -> Boolean = { true }
<lambda>null248 ): T = history.poll(defaultTimeoutMs, mark) { it is T && predicate(it) }.also {
249 assertNotNull(it, errorMsg)
250 } as T
251