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.net.MacAddress
25 import android.net.wifi.WifiManager
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 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
56     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     }
85 
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)
102         // 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)
106     }
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         }
114         // 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)
311     }
312 }
313