/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.snippet.connectivity import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.MacAddress import android.net.wifi.WifiManager import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDeviceList import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pManager import androidx.test.platform.app.InstrumentationRegistry import com.android.net.module.util.ArrayTrackRecord import com.android.testutils.runAsShell import com.google.android.mobly.snippet.Snippet import com.google.android.mobly.snippet.rpc.Rpc import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import kotlin.test.assertNotNull import kotlin.test.fail private const val TIMEOUT_MS = 60000L class Wifip2pMultiDevicesSnippet : Snippet { private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() } private val wifiManager by lazy { context.getSystemService(WifiManager::class.java) ?: fail("Could not get WifiManager service") } private val wifip2pManager by lazy { context.getSystemService(WifiP2pManager::class.java) ?: fail("Could not get WifiP2pManager service") } private lateinit var wifip2pChannel: WifiP2pManager.Channel private val wifip2pIntentReceiver = Wifip2pIntentReceiver() private class Wifip2pIntentReceiver : BroadcastReceiver() { val history = ArrayTrackRecord().newReadHead() sealed class IntentReceivedEvent { abstract val intent: Intent data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent() data class PeersChanged(override val intent: Intent) : IntentReceivedEvent() } override fun onReceive(context: Context, intent: Intent) { when (intent.action) { WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { history.add(ConnectionChanged(intent)) } WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { history.add(PeersChanged(intent)) } } } inline fun eventuallyExpectedIntent( timeoutMs: Long = TIMEOUT_MS, crossinline predicate: (T) -> Boolean = { true } ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also { assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.") } as T } @Rpc(description = "Check whether the device supports Wi-Fi P2P.") fun isP2pSupported() = wifiManager.isP2pSupported() @Rpc(description = "Start Wi-Fi P2P") fun startWifiP2p() { // Initialize Wi-Fi P2P wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null) // Ensure the Wi-Fi P2P channel is available val p2pStateEnabledFuture = CompletableFuture() wifip2pManager.requestP2pState(wifip2pChannel) { state -> if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { p2pStateEnabledFuture.complete(true) } } p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) // Register an intent filter to receive Wi-Fi P2P intents val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) context.registerReceiver(wifip2pIntentReceiver, filter) } @Rpc(description = "Stop Wi-Fi P2P") fun stopWifiP2p() { if (this::wifip2pChannel.isInitialized) { wifip2pManager.cancelConnect(wifip2pChannel, null) wifip2pManager.removeGroup(wifip2pChannel, null) } // Unregister the intent filter context.unregisterReceiver(wifip2pIntentReceiver) } @Rpc(description = "Get the current device name") fun getDeviceName(): String { // Retrieve current device info val deviceFuture = CompletableFuture() wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice -> if (wifiP2pDevice != null) { deviceFuture.complete(wifiP2pDevice.deviceName) } } // Return current device name return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } @Rpc(description = "Wait for a p2p connection changed intent and check the group") @Suppress("DEPRECATION") fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) { wifip2pIntentReceiver.eventuallyExpectedIntent() { val p2pGroup: WifiP2pGroup? = it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP) val groupMatched = p2pGroup?.networkName == groupName return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched } } @Rpc(description = "Create a Wi-Fi P2P group") fun createGroup(groupName: String, groupPassphrase: String) { // Create a Wi-Fi P2P group val wifip2pConfig = WifiP2pConfig.Builder() .setNetworkName(groupName) .setPassphrase(groupPassphrase) .build() val createGroupFuture = CompletableFuture() wifip2pManager.createGroup( wifip2pChannel, wifip2pConfig, object : WifiP2pManager.ActionListener { override fun onFailure(reason: Int) = Unit override fun onSuccess() { createGroupFuture.complete(true) } } ) createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) // Ensure the Wi-Fi P2P group is created. waitForP2pConnectionChanged(false, groupName) } @Rpc(description = "Start Wi-Fi P2P peers discovery") fun startPeersDiscovery() { // Start discovery Wi-Fi P2P peers wifip2pManager.discoverPeers(wifip2pChannel, null) // Ensure the discovery is started val p2pDiscoveryStartedFuture = CompletableFuture() wifip2pManager.requestDiscoveryState(wifip2pChannel) { state -> if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) { p2pDiscoveryStartedFuture.complete(true) } } p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } /** * Get the device address from the given intent that matches the given device name. * * @param peersChangedIntent the intent to get the device address from * @param deviceName the target device name * @return the address of the target device or null if no devices match. */ @Suppress("DEPRECATION") private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? { val peers: WifiP2pDeviceList? = peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST) return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress } /** * Ensure the given device has been discovered and returns the associated device address for * connection. * * @param deviceName the target device name * @return the address of the target device. */ @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered") fun ensureDeviceDiscovered(deviceName: String): String { val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent() { return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null } return getDeviceAddress(changedEvent.intent, deviceName) ?: fail("Missing device in filtered intent") } @Rpc(description = "Invite a Wi-Fi P2P device to the group") fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) { // Connect to the device to send invitation val wifip2pConfig = WifiP2pConfig.Builder() .setNetworkName(groupName) .setPassphrase(groupPassphrase) .setDeviceAddress(MacAddress.fromString(deviceAddress)) .build() val connectedFuture = CompletableFuture() wifip2pManager.connect( wifip2pChannel, wifip2pConfig, object : WifiP2pManager.ActionListener { override fun onFailure(reason: Int) = Unit override fun onSuccess() { connectedFuture.complete(true) } } ) connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } private fun runExternalApproverForGroupProcess( deviceAddress: String, isGroupInvitation: Boolean ) { val peer = MacAddress.fromString(deviceAddress) runAsShell(MANAGE_WIFI_NETWORK_SELECTION) { val connectionRequestFuture = CompletableFuture() val attachedFuture = CompletableFuture() wifip2pManager.addExternalApprover( wifip2pChannel, peer, object : WifiP2pManager.ExternalApproverRequestListener { override fun onAttached(deviceAddress: MacAddress) { attachedFuture.complete(true) } override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit override fun onConnectionRequested( requestType: Int, config: WifiP2pConfig, device: WifiP2pDevice ) { connectionRequestFuture.complete(true) } override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit } ) if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) val resultFuture = CompletableFuture() wifip2pManager.setConnectionRequestResult( wifip2pChannel, peer, WifiP2pManager.CONNECTION_REQUEST_ACCEPT, object : WifiP2pManager.ActionListener { override fun onFailure(reason: Int) = Unit override fun onSuccess() { resultFuture.complete(true) } } ) resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) val removeFuture = CompletableFuture() wifip2pManager.removeExternalApprover( wifip2pChannel, peer, object : WifiP2pManager.ActionListener { override fun onFailure(reason: Int) = Unit override fun onSuccess() { removeFuture.complete(true) } } ) removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } } @Rpc(description = "Accept P2P group invitation from device") fun acceptGroupInvitation(deviceAddress: String) { // Accept the Wi-Fi P2P group invitation runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */) } @Rpc(description = "Wait for connection request from the peer and accept joining") fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) { // Wait for connection request from the peer and accept joining runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */) } @Rpc(description = "Ensure the target device is connected") fun ensureDeviceConnected(deviceName: String) { // Retrieve peers and ensure the target device is connected val connectedFuture = CompletableFuture() wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any { it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let { connectedFuture.complete(true) } } connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } }