blob: f8c9351fc1668eaf3dd80091c8b5f5a52876af16 [file] [log] [blame]
Paul Huf5020472024-07-04 07:51:22 +00001/*
2 * Copyright (C) 2024 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
17package com.google.snippet.connectivity
18
Paul Hu3e24dec2024-07-04 08:54:45 +000019import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION
20import android.content.BroadcastReceiver
21import android.content.Context
22import android.content.Intent
23import android.content.IntentFilter
24import android.net.MacAddress
Paul Huf5020472024-07-04 07:51:22 +000025import android.net.wifi.WifiManager
Paul Hu3e24dec2024-07-04 08:54:45 +000026import android.net.wifi.p2p.WifiP2pConfig
27import android.net.wifi.p2p.WifiP2pDevice
28import android.net.wifi.p2p.WifiP2pDeviceList
29import android.net.wifi.p2p.WifiP2pGroup
Paul Huf5020472024-07-04 07:51:22 +000030import android.net.wifi.p2p.WifiP2pManager
31import androidx.test.platform.app.InstrumentationRegistry
Paul Hu3e24dec2024-07-04 08:54:45 +000032import com.android.net.module.util.ArrayTrackRecord
33import com.android.testutils.runAsShell
Paul Huf5020472024-07-04 07:51:22 +000034import com.google.android.mobly.snippet.Snippet
35import com.google.android.mobly.snippet.rpc.Rpc
Paul Hu3e24dec2024-07-04 08:54:45 +000036import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
37import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
Paul Huf5020472024-07-04 07:51:22 +000038import java.util.concurrent.CompletableFuture
39import java.util.concurrent.TimeUnit
Paul Hu3e24dec2024-07-04 08:54:45 +000040import kotlin.test.assertNotNull
Paul Huf5020472024-07-04 07:51:22 +000041import kotlin.test.fail
42
43private const val TIMEOUT_MS = 60000L
44
45class Wifip2pMultiDevicesSnippet : Snippet {
46 private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
47 private val wifiManager by lazy {
48 context.getSystemService(WifiManager::class.java)
49 ?: fail("Could not get WifiManager service")
50 }
51 private val wifip2pManager by lazy {
52 context.getSystemService(WifiP2pManager::class.java)
53 ?: fail("Could not get WifiP2pManager service")
54 }
55 private lateinit var wifip2pChannel: WifiP2pManager.Channel
Paul Hu3e24dec2024-07-04 08:54:45 +000056 private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
57
58 private class Wifip2pIntentReceiver : BroadcastReceiver() {
59 val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
60
61 sealed class IntentReceivedEvent {
62 abstract val intent: Intent
63 data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
64 data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
65 }
66
67 override fun onReceive(context: Context, intent: Intent) {
68 when (intent.action) {
69 WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
70 history.add(ConnectionChanged(intent))
71 }
72 WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
73 history.add(PeersChanged(intent))
74 }
75 }
76 }
77
78 inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
79 timeoutMs: Long = TIMEOUT_MS,
80 crossinline predicate: (T) -> Boolean = { true }
81 ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
82 assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
83 } as T
84 }
Paul Huf5020472024-07-04 07:51:22 +000085
86 @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
87 fun isP2pSupported() = wifiManager.isP2pSupported()
88
89 @Rpc(description = "Start Wi-Fi P2P")
90 fun startWifiP2p() {
91 // Initialize Wi-Fi P2P
92 wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null)
93
94 // Ensure the Wi-Fi P2P channel is available
95 val p2pStateEnabledFuture = CompletableFuture<Boolean>()
96 wifip2pManager.requestP2pState(wifip2pChannel) { state ->
97 if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
98 p2pStateEnabledFuture.complete(true)
99 }
100 }
101 p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
Paul Hu3e24dec2024-07-04 08:54:45 +0000102 // Register an intent filter to receive Wi-Fi P2P intents
103 val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
104 filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
105 context.registerReceiver(wifip2pIntentReceiver, filter)
Paul Huf5020472024-07-04 07:51:22 +0000106 }
107
108 @Rpc(description = "Stop Wi-Fi P2P")
109 fun stopWifiP2p() {
110 if (this::wifip2pChannel.isInitialized) {
111 wifip2pManager.cancelConnect(wifip2pChannel, null)
112 wifip2pManager.removeGroup(wifip2pChannel, null)
113 }
Paul Hu3e24dec2024-07-04 08:54:45 +0000114 // Unregister the intent filter
115 context.unregisterReceiver(wifip2pIntentReceiver)
116 }
117
118 @Rpc(description = "Get the current device name")
119 fun getDeviceName(): String {
120 // Retrieve current device info
121 val deviceFuture = CompletableFuture<String>()
122 wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
123 if (wifiP2pDevice != null) {
124 deviceFuture.complete(wifiP2pDevice.deviceName)
125 }
126 }
127 // Return current device name
128 return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
129 }
130
131 @Rpc(description = "Wait for a p2p connection changed intent and check the group")
132 @Suppress("DEPRECATION")
133 fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
134 wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
135 val p2pGroup: WifiP2pGroup? =
136 it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
137 val groupMatched = p2pGroup?.networkName == groupName
138 return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
139 }
140 }
141
142 @Rpc(description = "Create a Wi-Fi P2P group")
143 fun createGroup(groupName: String, groupPassphrase: String) {
144 // Create a Wi-Fi P2P group
145 val wifip2pConfig = WifiP2pConfig.Builder()
146 .setNetworkName(groupName)
147 .setPassphrase(groupPassphrase)
148 .build()
149 val createGroupFuture = CompletableFuture<Boolean>()
150 wifip2pManager.createGroup(
151 wifip2pChannel,
152 wifip2pConfig,
153 object : WifiP2pManager.ActionListener {
154 override fun onFailure(reason: Int) = Unit
155 override fun onSuccess() { createGroupFuture.complete(true) }
156 }
157 )
158 createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
159
160 // Ensure the Wi-Fi P2P group is created.
161 waitForP2pConnectionChanged(false, groupName)
162 }
163
164 @Rpc(description = "Start Wi-Fi P2P peers discovery")
165 fun startPeersDiscovery() {
166 // Start discovery Wi-Fi P2P peers
167 wifip2pManager.discoverPeers(wifip2pChannel, null)
168
169 // Ensure the discovery is started
170 val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
171 wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
172 if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
173 p2pDiscoveryStartedFuture.complete(true)
174 }
175 }
176 p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
177 }
178
179 /**
180 * Get the device address from the given intent that matches the given device name.
181 *
182 * @param peersChangedIntent the intent to get the device address from
183 * @param deviceName the target device name
184 * @return the address of the target device or null if no devices match.
185 */
186 @Suppress("DEPRECATION")
187 private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
188 val peers: WifiP2pDeviceList? =
189 peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
190 return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
191 }
192
193 /**
194 * Ensure the given device has been discovered and returns the associated device address for
195 * connection.
196 *
197 * @param deviceName the target device name
198 * @return the address of the target device.
199 */
200 @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
201 fun ensureDeviceDiscovered(deviceName: String): String {
202 val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
203 return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
204 }
205 return getDeviceAddress(changedEvent.intent, deviceName)
206 ?: fail("Missing device in filtered intent")
207 }
208
209 @Rpc(description = "Invite a Wi-Fi P2P device to the group")
210 fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
211 // Connect to the device to send invitation
212 val wifip2pConfig = WifiP2pConfig.Builder()
213 .setNetworkName(groupName)
214 .setPassphrase(groupPassphrase)
215 .setDeviceAddress(MacAddress.fromString(deviceAddress))
216 .build()
217 val connectedFuture = CompletableFuture<Boolean>()
218 wifip2pManager.connect(
219 wifip2pChannel,
220 wifip2pConfig,
221 object : WifiP2pManager.ActionListener {
222 override fun onFailure(reason: Int) = Unit
223 override fun onSuccess() {
224 connectedFuture.complete(true)
225 }
226 }
227 )
228 connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
229 }
230
231 private fun runExternalApproverForGroupProcess(
232 deviceAddress: String,
233 isGroupInvitation: Boolean
234 ) {
235 val peer = MacAddress.fromString(deviceAddress)
236 runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
237 val connectionRequestFuture = CompletableFuture<Boolean>()
238 val attachedFuture = CompletableFuture<Boolean>()
239 wifip2pManager.addExternalApprover(
240 wifip2pChannel,
241 peer,
242 object : WifiP2pManager.ExternalApproverRequestListener {
243 override fun onAttached(deviceAddress: MacAddress) {
244 attachedFuture.complete(true)
245 }
246 override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
247 override fun onConnectionRequested(
248 requestType: Int,
249 config: WifiP2pConfig,
250 device: WifiP2pDevice
251 ) {
252 connectionRequestFuture.complete(true)
253 }
254 override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
255 }
256 )
257 if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
258 connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
259
260 val resultFuture = CompletableFuture<Boolean>()
261 wifip2pManager.setConnectionRequestResult(
262 wifip2pChannel,
263 peer,
264 WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
265 object : WifiP2pManager.ActionListener {
266 override fun onFailure(reason: Int) = Unit
267 override fun onSuccess() {
268 resultFuture.complete(true)
269 }
270 }
271 )
272 resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
273
274 val removeFuture = CompletableFuture<Boolean>()
275 wifip2pManager.removeExternalApprover(
276 wifip2pChannel,
277 peer,
278 object : WifiP2pManager.ActionListener {
279 override fun onFailure(reason: Int) = Unit
280 override fun onSuccess() {
281 removeFuture.complete(true)
282 }
283 }
284 )
285 removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
286 }
287 }
288
289 @Rpc(description = "Accept P2P group invitation from device")
290 fun acceptGroupInvitation(deviceAddress: String) {
291 // Accept the Wi-Fi P2P group invitation
292 runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
293 }
294
295 @Rpc(description = "Wait for connection request from the peer and accept joining")
296 fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
297 // Wait for connection request from the peer and accept joining
298 runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
299 }
300
301 @Rpc(description = "Ensure the target device is connected")
302 fun ensureDeviceConnected(deviceName: String) {
303 // Retrieve peers and ensure the target device is connected
304 val connectedFuture = CompletableFuture<Boolean>()
305 wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
306 it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
307 connectedFuture.complete(true)
308 }
309 }
310 connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
Paul Huf5020472024-07-04 07:51:22 +0000311 }
312}