• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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