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