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