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 private val TAG = ConnectUtil::class.java.simpleName
57
58 private val cm = context.getSystemService(ConnectivityManager::class.java)
59 ?: fail("Could not find ConnectivityManager")
60 private val wifiManager = context.getSystemService(WifiManager::class.java)
61 ?: fail("Could not find WifiManager")
62
ensureWifiConnectednull63 fun ensureWifiConnected(): Network = ensureWifiConnected(requireValidated = false)
64 fun ensureWifiValidated(): Network = ensureWifiConnected(requireValidated = true)
65
66 fun ensureCellularValidated(): Network {
67 val cb = TestableNetworkCallback()
68 cm.requestNetwork(
69 NetworkRequest.Builder()
70 .addTransportType(TRANSPORT_CELLULAR)
71 .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
72 return tryTest {
73 val errorMsg = "The device does not have mobile data available. Check that it is " +
74 "setup with a SIM card that has a working data plan, that the APN " +
75 "configuration is valid, and that the device can access the internet through " +
76 "mobile data."
77 cb.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
78 it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
79 }.network
80 } cleanup {
81 cm.unregisterNetworkCallback(cb)
82 }
83 }
84
ensureWifiConnectednull85 private fun ensureWifiConnected(requireValidated: Boolean): Network {
86 val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
87 cm.registerNetworkCallback(NetworkRequest.Builder()
88 .addTransportType(TRANSPORT_WIFI)
89 .addCapability(NET_CAPABILITY_INTERNET)
90 .build(), callback)
91
92 return tryTest {
93 val connInfo = wifiManager.connectionInfo
94 Log.d(TAG, "connInfo=" + connInfo)
95 if (connInfo == null || connInfo.networkId == -1) {
96 clearWifiBlocklist()
97 val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
98 // Read the output stream to ensure the command has completed
99 ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
100 val config = getOrCreateWifiConfiguration()
101 connectToWifiConfig(config)
102 }
103 val errorMsg = if (requireValidated) {
104 "The wifi access point did not have access to the internet after " +
105 "$WIFI_CONNECT_TIMEOUT_MS ms. Check that it has a working connection."
106 } else {
107 "Could not connect to a wifi access point within $WIFI_CONNECT_TIMEOUT_MS ms. " +
108 "Check that the test device has a wifi network configured, and that the " +
109 "test access point is functioning properly."
110 }
111 val cb = callback.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
112 (!requireValidated || it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
113 }
114 cb.network
115 } cleanup {
116 cm.unregisterNetworkCallback(callback)
117 }
118 }
119
120 // Suppress warning because WifiManager methods to connect to a config are
121 // documented not to be deprecated for privileged users.
122 @Suppress("DEPRECATION")
connectToWifiConfignull123 fun connectToWifiConfig(config: WifiConfiguration) {
124 repeat(MAX_WIFI_CONNECT_RETRIES) {
125 val error = runAsShell(permission.NETWORK_SETTINGS) {
126 val listener = ConnectWifiListener()
127 wifiManager.connect(config, listener)
128 listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
129 } ?: return // Connect succeeded
130
131 // Only retry for IN_PROGRESS and BUSY
132 if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
133 fail("Failed to connect to " + config.SSID + ": " + error)
134 }
135 Log.w(TAG, "connect failed with $error; waiting before retry")
136 SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
137 }
138 fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
139 }
140
141 private class ConnectWifiListener : WifiManager.ActionListener {
142 /**
143 * Future completed when the connect process ends. Provides the error code or null if none.
144 */
145 val connectFuture = CompletableFuture<Int?>()
onSuccessnull146 override fun onSuccess() {
147 connectFuture.complete(null)
148 }
149
onFailurenull150 override fun onFailure(reason: Int) {
151 connectFuture.complete(reason)
152 }
153 }
154
getOrCreateWifiConfigurationnull155 private fun getOrCreateWifiConfiguration(): WifiConfiguration {
156 val configs = runAsShell(permission.NETWORK_SETTINGS) {
157 wifiManager.getConfiguredNetworks()
158 }
159 // If no network is configured, add a config for virtual access points if applicable
160 if (configs.size == 0) {
161 val scanResults = getWifiScanResults()
162 val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
163 assertNotNull(virtualConfig, "The device has no configured wifi network")
164 return virtualConfig
165 }
166 // No need to add a configuration: there is already one.
167 if (configs.size > 1) {
168 // For convenience in case of local testing on devices with multiple saved configs,
169 // prefer the first configuration that is in range.
170 // In actual tests, there should only be one configuration, and it should be usable as
171 // assumed by WifiManagerTest.testConnect.
172 Log.w(TAG, "Multiple wifi configurations found: " +
173 configs.joinToString(", ") { it.SSID })
174 val scanResultsList = getWifiScanResults()
175 Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
176 "${it.SSID} (${it.level})"
177 })
178
179 val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
180 return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
181 }
182 return configs[0]
183 }
184
getWifiScanResultsnull185 private fun getWifiScanResults(): List<ScanResult> {
186 val scanResultsFuture = CompletableFuture<List<ScanResult>>()
187 runAsShell(permission.NETWORK_SETTINGS) {
188 val receiver: BroadcastReceiver = object : BroadcastReceiver() {
189 override fun onReceive(context: Context, intent: Intent) {
190 scanResultsFuture.complete(wifiManager.scanResults)
191 }
192 }
193 context.registerReceiver(receiver,
194 IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
195 wifiManager.startScan()
196 }
197 return try {
198 scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
199 } catch (e: Exception) {
200 throw AssertionError("Wifi scan results not received within timeout", e)
201 }
202 }
203
204 /**
205 * If a virtual wifi network is detected, add a configuration for that network.
206 * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
207 */
maybeConfigureVirtualNetworknull208 private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
209 // Virtual wifi networks used on the emulator and cloud testing infrastructure
210 val virtualSsids = listOf("VirtWifi", "AndroidWifi")
211 Log.d(TAG, "Wifi scan results: $scanResults")
212 val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
213 ?: return null
214
215 // Only add the virtual configuration if the virtual AP is detected in scans
216 val virtualConfig = WifiConfiguration()
217 // ASCII SSIDs need to be surrounded by double quotes
218 virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
219 virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
220 runAsShell(permission.NETWORK_SETTINGS) {
221 val networkId = wifiManager.addNetwork(virtualConfig)
222 assertTrue(networkId >= 0)
223 assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
224 }
225 return virtualConfig
226 }
227
228 /**
229 * Re-enable wifi networks that were blocked, typically because no internet connection was
230 * detected the last time they were connected. This is necessary to make sure wifi can reconnect
231 * to them.
232 */
clearWifiBlocklistnull233 private fun clearWifiBlocklist() {
234 runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
235 for (cfg in wifiManager.configuredNetworks) {
236 assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
237 }
238 }
239 }
240 }
241
eventuallyExpectnull242 private inline fun <reified T : CallbackEntry> TestableNetworkCallback.eventuallyExpect(
243 errorMsg: String,
244 crossinline predicate: (T) -> Boolean = { true }
<lambda>null245 ): T = history.poll(defaultTimeoutMs, mark) { it is T && predicate(it) }.also {
246 assertNotNull(it, errorMsg)
247 } as T
248