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