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.TRANSPORT_WIFI 27 import android.net.NetworkRequest 28 import android.net.wifi.ScanResult 29 import android.net.wifi.WifiConfiguration 30 import android.net.wifi.WifiManager 31 import android.os.ParcelFileDescriptor 32 import android.os.SystemClock 33 import android.util.Log 34 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 35 import com.android.testutils.RecorderCallback.CallbackEntry 36 import java.util.concurrent.CompletableFuture 37 import java.util.concurrent.TimeUnit 38 import kotlin.test.assertNotNull 39 import kotlin.test.assertTrue 40 import kotlin.test.fail 41 42 private const val MAX_WIFI_CONNECT_RETRIES = 10 43 private const val WIFI_CONNECT_INTERVAL_MS = 500L 44 private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L 45 46 // Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi, 47 // the error code constants are not (b/204277752) 48 private const val WIFI_ERROR_IN_PROGRESS = 1 49 private const val WIFI_ERROR_BUSY = 2 50 51 class ConnectUtil(private val context: Context) { 52 private val TAG = ConnectUtil::class.java.simpleName 53 54 private val cm = context.getSystemService(ConnectivityManager::class.java) 55 ?: fail("Could not find ConnectivityManager") 56 private val wifiManager = context.getSystemService(WifiManager::class.java) 57 ?: fail("Could not find WifiManager") 58 ensureWifiConnectednull59 fun ensureWifiConnected(): Network { 60 val callback = TestableNetworkCallback() 61 cm.registerNetworkCallback(NetworkRequest.Builder() 62 .addTransportType(TRANSPORT_WIFI) 63 .build(), callback) 64 65 try { 66 val connInfo = wifiManager.connectionInfo 67 Log.d(TAG, "connInfo=" + connInfo) 68 if (connInfo == null || connInfo.networkId == -1) { 69 clearWifiBlocklist() 70 val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable") 71 // Read the output stream to ensure the command has completed 72 ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() } 73 val config = getOrCreateWifiConfiguration() 74 connectToWifiConfig(config) 75 } 76 val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available } 77 assertNotNull(cb, "Could not connect to a wifi access point within " + 78 "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " + 79 "configured, and that the test access point is functioning properly.") 80 return cb.network 81 } finally { 82 cm.unregisterNetworkCallback(callback) 83 } 84 } 85 connectToWifiConfignull86 private fun connectToWifiConfig(config: WifiConfiguration) { 87 repeat(MAX_WIFI_CONNECT_RETRIES) { 88 val error = runAsShell(permission.NETWORK_SETTINGS) { 89 val listener = ConnectWifiListener() 90 wifiManager.connect(config, listener) 91 listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) 92 } ?: return // Connect succeeded 93 94 // Only retry for IN_PROGRESS and BUSY 95 if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) { 96 fail("Failed to connect to " + config.SSID + ": " + error) 97 } 98 Log.w(TAG, "connect failed with $error; waiting before retry") 99 SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS) 100 } 101 fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries") 102 } 103 104 private class ConnectWifiListener : WifiManager.ActionListener { 105 /** 106 * Future completed when the connect process ends. Provides the error code or null if none. 107 */ 108 val connectFuture = CompletableFuture<Int?>() onSuccessnull109 override fun onSuccess() { 110 connectFuture.complete(null) 111 } 112 onFailurenull113 override fun onFailure(reason: Int) { 114 connectFuture.complete(reason) 115 } 116 } 117 getOrCreateWifiConfigurationnull118 private fun getOrCreateWifiConfiguration(): WifiConfiguration { 119 val configs = runAsShell(permission.NETWORK_SETTINGS) { 120 wifiManager.getConfiguredNetworks() 121 } 122 // If no network is configured, add a config for virtual access points if applicable 123 if (configs.size == 0) { 124 val scanResults = getWifiScanResults() 125 val virtualConfig = maybeConfigureVirtualNetwork(scanResults) 126 assertNotNull(virtualConfig, "The device has no configured wifi network") 127 return virtualConfig 128 } 129 // No need to add a configuration: there is already one. 130 if (configs.size > 1) { 131 // For convenience in case of local testing on devices with multiple saved configs, 132 // prefer the first configuration that is in range. 133 // In actual tests, there should only be one configuration, and it should be usable as 134 // assumed by WifiManagerTest.testConnect. 135 Log.w(TAG, "Multiple wifi configurations found: " + 136 configs.joinToString(", ") { it.SSID }) 137 val scanResultsList = getWifiScanResults() 138 Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") { 139 "${it.SSID} (${it.level})" 140 }) 141 142 val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet() 143 return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0] 144 } 145 return configs[0] 146 } 147 getWifiScanResultsnull148 private fun getWifiScanResults(): List<ScanResult> { 149 val scanResultsFuture = CompletableFuture<List<ScanResult>>() 150 runAsShell(permission.NETWORK_SETTINGS) { 151 val receiver: BroadcastReceiver = object : BroadcastReceiver() { 152 override fun onReceive(context: Context, intent: Intent) { 153 scanResultsFuture.complete(wifiManager.scanResults) 154 } 155 } 156 context.registerReceiver(receiver, 157 IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) 158 wifiManager.startScan() 159 } 160 return try { 161 scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) 162 } catch (e: Exception) { 163 throw AssertionError("Wifi scan results not received within timeout", e) 164 } 165 } 166 167 /** 168 * If a virtual wifi network is detected, add a configuration for that network. 169 * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate. 170 */ maybeConfigureVirtualNetworknull171 private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? { 172 // Virtual wifi networks used on the emulator and cloud testing infrastructure 173 val virtualSsids = listOf("VirtWifi", "AndroidWifi") 174 Log.d(TAG, "Wifi scan results: $scanResults") 175 val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) } 176 ?: return null 177 178 // Only add the virtual configuration if the virtual AP is detected in scans 179 val virtualConfig = WifiConfiguration() 180 // ASCII SSIDs need to be surrounded by double quotes 181 virtualConfig.SSID = "\"${virtualScanResult.SSID}\"" 182 virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE) 183 runAsShell(permission.NETWORK_SETTINGS) { 184 val networkId = wifiManager.addNetwork(virtualConfig) 185 assertTrue(networkId >= 0) 186 assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */)) 187 } 188 return virtualConfig 189 } 190 191 /** 192 * Re-enable wifi networks that were blocked, typically because no internet connection was 193 * detected the last time they were connected. This is necessary to make sure wifi can reconnect 194 * to them. 195 */ clearWifiBlocklistnull196 private fun clearWifiBlocklist() { 197 runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) { 198 for (cfg in wifiManager.configuredNetworks) { 199 assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */)) 200 } 201 } 202 } 203 } 204