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