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