summaryrefslogtreecommitdiff
path: root/common/testutils/devicetests/com/android/testutils/ConnectUtil.kt
blob: 71f7877e1a32675c6b5fc4970df3b2d4f64e0ba8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.testutils

import android.Manifest.permission
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.wifi.ScanResult
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.testutils.RecorderCallback.CallbackEntry
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail

private const val MAX_WIFI_CONNECT_RETRIES = 10
private const val WIFI_CONNECT_INTERVAL_MS = 500L
private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L

// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
// the error code constants are not (b/204277752)
private const val WIFI_ERROR_IN_PROGRESS = 1
private const val WIFI_ERROR_BUSY = 2

class ConnectUtil(private val context: Context) {
    private val TAG = ConnectUtil::class.java.simpleName

    private val cm = context.getSystemService(ConnectivityManager::class.java)
            ?: fail("Could not find ConnectivityManager")
    private val wifiManager = context.getSystemService(WifiManager::class.java)
            ?: fail("Could not find WifiManager")

    fun ensureWifiConnected(): Network {
        val callback = TestableNetworkCallback()
        cm.registerNetworkCallback(NetworkRequest.Builder()
                .addTransportType(TRANSPORT_WIFI)
                .build(), callback)

        try {
            val connInfo = wifiManager.connectionInfo
            Log.d(TAG, "connInfo=" + connInfo)
            if (connInfo == null || connInfo.networkId == -1) {
                clearWifiBlocklist()
                val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
                // Read the output stream to ensure the command has completed
                ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
                val config = getOrCreateWifiConfiguration()
                connectToWifiConfig(config)
            }
            val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
            assertNotNull(cb, "Could not connect to a wifi access point within " +
                    "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
                    "configured, and that the test access point is functioning properly.")
            return cb.network
        } finally {
            cm.unregisterNetworkCallback(callback)
        }
    }

    private fun connectToWifiConfig(config: WifiConfiguration) {
        repeat(MAX_WIFI_CONNECT_RETRIES) {
            val error = runAsShell(permission.NETWORK_SETTINGS) {
                val listener = ConnectWifiListener()
                wifiManager.connect(config, listener)
                listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
            } ?: return // Connect succeeded

            // Only retry for IN_PROGRESS and BUSY
            if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
                fail("Failed to connect to " + config.SSID + ": " + error)
            }
            Log.w(TAG, "connect failed with $error; waiting before retry")
            SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
        }
        fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
    }

    private class ConnectWifiListener : WifiManager.ActionListener {
        /**
         * Future completed when the connect process ends. Provides the error code or null if none.
         */
        val connectFuture = CompletableFuture<Int?>()
        override fun onSuccess() {
            connectFuture.complete(null)
        }

        override fun onFailure(reason: Int) {
            connectFuture.complete(reason)
        }
    }

    private fun getOrCreateWifiConfiguration(): WifiConfiguration {
        val configs = runAsShell(permission.NETWORK_SETTINGS) {
            wifiManager.getConfiguredNetworks()
        }
        // If no network is configured, add a config for virtual access points if applicable
        if (configs.size == 0) {
            val scanResults = getWifiScanResults()
            val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
            assertNotNull(virtualConfig, "The device has no configured wifi network")
            return virtualConfig
        }
        // No need to add a configuration: there is already one.
        if (configs.size > 1) {
            // For convenience in case of local testing on devices with multiple saved configs,
            // prefer the first configuration that is in range.
            // In actual tests, there should only be one configuration, and it should be usable as
            // assumed by WifiManagerTest.testConnect.
            Log.w(TAG, "Multiple wifi configurations found: " +
                    configs.joinToString(", ") { it.SSID })
            val scanResultsList = getWifiScanResults()
            Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
                "${it.SSID} (${it.level})"
            })

            val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
            return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
        }
        return configs[0]
    }

    private fun getWifiScanResults(): List<ScanResult> {
        val scanResultsFuture = CompletableFuture<List<ScanResult>>()
        runAsShell(permission.NETWORK_SETTINGS) {
            val receiver: BroadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context, intent: Intent) {
                    scanResultsFuture.complete(wifiManager.scanResults)
                }
            }
            context.registerReceiver(receiver,
                    IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
            wifiManager.startScan()
        }
        return try {
            scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
        } catch (e: Exception) {
            throw AssertionError("Wifi scan results not received within timeout", e)
        }
    }

    /**
     * If a virtual wifi network is detected, add a configuration for that network.
     * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
     */
    private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
        // Virtual wifi networks used on the emulator and cloud testing infrastructure
        val virtualSsids = listOf("VirtWifi", "AndroidWifi")
        Log.d(TAG, "Wifi scan results: $scanResults")
        val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
                ?: return null

        // Only add the virtual configuration if the virtual AP is detected in scans
        val virtualConfig = WifiConfiguration()
        // ASCII SSIDs need to be surrounded by double quotes
        virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
        virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
        runAsShell(permission.NETWORK_SETTINGS) {
            val networkId = wifiManager.addNetwork(virtualConfig)
            assertTrue(networkId >= 0)
            assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
        }
        return virtualConfig
    }

    /**
     * Re-enable wifi networks that were blocked, typically because no internet connection was
     * detected the last time they were connected. This is necessary to make sure wifi can reconnect
     * to them.
     */
    private fun clearWifiBlocklist() {
        runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
            for (cfg in wifiManager.configuredNetworks) {
                assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
            }
        }
    }
}