1 /* 2 * Copyright (C) 2023 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.wifi.direct; 18 19 import android.Manifest; 20 import android.app.Instrumentation; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.pm.PackageManager; 26 import android.net.NetworkInfo; 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.WifiP2pGroupList; 31 import android.net.wifi.p2p.WifiP2pInfo; 32 import android.net.wifi.p2p.WifiP2pManager; 33 import android.os.Bundle; 34 import android.widget.Button; 35 36 import androidx.annotation.NonNull; 37 import androidx.test.core.app.ApplicationProvider; 38 import androidx.test.platform.app.InstrumentationRegistry; 39 import androidx.test.uiautomator.By; 40 import androidx.test.uiautomator.UiDevice; 41 import androidx.test.uiautomator.UiObject2; 42 import androidx.test.uiautomator.Until; 43 44 import com.google.android.mobly.snippet.Snippet; 45 import com.google.android.mobly.snippet.event.EventCache; 46 import com.google.android.mobly.snippet.event.SnippetEvent; 47 import com.google.android.mobly.snippet.rpc.AsyncRpc; 48 import com.google.android.mobly.snippet.rpc.Rpc; 49 import com.google.android.mobly.snippet.rpc.RpcOptional; 50 import com.google.android.mobly.snippet.util.Log; 51 52 import org.json.JSONObject; 53 54 import java.util.ArrayList; 55 import java.util.UUID; 56 import java.util.concurrent.LinkedBlockingDeque; 57 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.TimeoutException; 59 import java.util.regex.Pattern; 60 61 62 /** Snippet class for WifiP2pManager. */ 63 public class WifiP2pManagerSnippet implements Snippet { 64 private static final int TIMEOUT_SHORT_MS = 10000; 65 private static final int UI_ACTION_SHORT_TIMEOUT_MS = 5000; 66 private static final int UI_ACTION_LONG_TIMEOUT_MS = 30000; 67 private static final String EVENT_KEY_CALLBACK_NAME = "callbackName"; 68 private static final String EVENT_KEY_REASON = "reason"; 69 private static final String EVENT_KEY_P2P_DEVICE = "p2pDevice"; 70 private static final String EVENT_KEY_P2P_INFO = "p2pInfo"; 71 private static final String EVENT_KEY_P2P_GROUP = "p2pGroup"; 72 private static final String EVENT_KEY_PEER_LIST = "peerList"; 73 private static final String ACTION_LISTENER_ON_SUCCESS = "onSuccess"; 74 public static final String ACTION_LISTENER_ON_FAILURE = "onFailure"; 75 76 private final Context mContext; 77 private final IntentFilter mIntentFilter; 78 private final WifiP2pManager mP2pManager; 79 80 private Instrumentation mInstrumentation = 81 InstrumentationRegistry.getInstrumentation(); 82 private UiDevice mUiDevice = UiDevice.getInstance(mInstrumentation); 83 84 private WifiP2pManager.Channel mChannel = null; 85 private WifiP2pStateChangedReceiver mStateChangedReceiver = null; 86 87 88 private static class WifiP2pManagerException extends Exception { WifiP2pManagerException(String message)89 WifiP2pManagerException(String message) { 90 super(message); 91 } 92 } 93 WifiP2pManagerSnippet()94 public WifiP2pManagerSnippet() { 95 Log.d("Elevating permission require to enable support for privileged operation in " 96 + "Android Q+"); 97 mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(); 98 99 mContext = ApplicationProvider.getApplicationContext(); 100 101 checkPermissions(mContext, Manifest.permission.ACCESS_WIFI_STATE, 102 Manifest.permission.CHANGE_WIFI_STATE, Manifest.permission.ACCESS_FINE_LOCATION, 103 Manifest.permission.NEARBY_WIFI_DEVICES 104 ); 105 106 mP2pManager = mContext.getSystemService(WifiP2pManager.class); 107 108 mIntentFilter = new IntentFilter(); 109 mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); 110 mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); 111 mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); 112 mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); 113 114 } 115 116 /** Register the application with the Wi-Fi framework. */ 117 @AsyncRpc(description = "Register the application with the Wi-Fi framework.") wifiP2pInitialize(String callbackId)118 public void wifiP2pInitialize(String callbackId) throws WifiP2pManagerException { 119 if (mChannel != null) { 120 throw new WifiP2pManagerException( 121 "Channel has already created, please close current section before initliaze a " 122 + "new one."); 123 124 } 125 checkP2pManager(); 126 mStateChangedReceiver = new WifiP2pStateChangedReceiver(callbackId); 127 mContext.registerReceiver( 128 mStateChangedReceiver, mIntentFilter, Context.RECEIVER_NOT_EXPORTED); 129 mChannel = mP2pManager.initialize(mContext, mContext.getMainLooper(), null); 130 } 131 132 /** Request the device information in the form of WifiP2pDevice. */ 133 @AsyncRpc(description = "Request the device information in the form of WifiP2pDevice.") wifiP2pRequestDeviceInfo(String callbackId)134 public void wifiP2pRequestDeviceInfo(String callbackId) throws WifiP2pManagerException { 135 checkChannel(); 136 mP2pManager.requestDeviceInfo(mChannel, new DeviceInfoListener(callbackId)); 137 } 138 139 /** 140 * Initiate peer discovery. A discovery process involves scanning for available Wi-Fi peers for 141 * the purpose of establishing a connection. 142 * 143 * @throws Throwable If this failed to initiate discovery, or the action timed out. 144 */ 145 @Rpc( 146 description = "Initiate peer discovery. A discovery process involves scanning for " 147 + "available Wi-Fi peers for the purpose of establishing a connection.") wifiP2pDiscoverPeers()148 public void wifiP2pDiscoverPeers() throws Throwable { 149 checkChannel(); 150 String callbackId = UUID.randomUUID().toString(); 151 mP2pManager.discoverPeers(mChannel, new ActionListener(callbackId)); 152 verifyActionListenerSucceed(callbackId); 153 } 154 155 /** 156 * Cancel any ongoing p2p group negotiation. 157 * 158 * @return The event posted by the callback methods of {@link ActionListener}. 159 */ 160 @Rpc(description = "Cancel any ongoing p2p group negotiation.") wifiP2pCancelConnect()161 public Bundle wifiP2pCancelConnect() throws Throwable { 162 checkChannel(); 163 String callbackId = UUID.randomUUID().toString(); 164 mP2pManager.cancelConnect(mChannel, new ActionListener((callbackId))); 165 return waitActionListenerResult(callbackId); 166 } 167 168 /** 169 * Stop current ongoing peer discovery. 170 * 171 * @return The event posted by the callback methods of {@link ActionListener}. 172 */ 173 @Rpc(description = "Stop current ongoing peer discovery.") wifiP2pStopPeerDiscovery()174 public Bundle wifiP2pStopPeerDiscovery() throws Throwable { 175 checkChannel(); 176 String callbackId = UUID.randomUUID().toString(); 177 mP2pManager.stopPeerDiscovery(mChannel, new ActionListener(callbackId)); 178 return waitActionListenerResult(callbackId); 179 } 180 181 /** 182 * Create a p2p group with the current device as the group owner. 183 * 184 * @throws Throwable If this failed to initiate discovery, or the action timed out. 185 */ 186 @AsyncRpc(description = "Create a p2p group with the current device as the group owner.") wifiP2pCreateGroup(String callbackId, @RpcOptional JSONObject wifiP2pConfig)187 public void wifiP2pCreateGroup(String callbackId, @RpcOptional JSONObject wifiP2pConfig) 188 throws Throwable { 189 checkChannel(); 190 ActionListener actionListener = new ActionListener(callbackId); 191 if (wifiP2pConfig == null) { 192 mP2pManager.createGroup(mChannel, actionListener); 193 } else { 194 mP2pManager.createGroup( 195 mChannel, JsonDeserializer.jsonToWifiP2pConfig(wifiP2pConfig), actionListener); 196 } 197 verifyActionListenerSucceed(callbackId); 198 } 199 200 /** 201 * Start a p2p connection to a device with the specified configuration. 202 * 203 * @throws Throwable If this failed to initiate discovery, or the action timed out. 204 */ 205 @Rpc(description = "Start a p2p connection to a device with the specified configuration.") wifiP2pConnect(JSONObject wifiP2pConfig)206 public void wifiP2pConnect(JSONObject wifiP2pConfig) throws Throwable { 207 checkChannel(); 208 String callbackId = UUID.randomUUID().toString(); 209 mP2pManager.connect(mChannel, JsonDeserializer.jsonToWifiP2pConfig(wifiP2pConfig), 210 new ActionListener(callbackId)); 211 verifyActionListenerSucceed(callbackId); 212 } 213 214 /** Accept p2p connection invitation through clicking on UI. */ 215 @Rpc(description = "Accept p2p connection invitation through clicking on UI.") wifiP2pAcceptInvitation(String deviceName)216 public void wifiP2pAcceptInvitation(String deviceName) throws WifiP2pManagerException { 217 if (!mUiDevice.wait(Until.hasObject(By.text("Invitation to connect")), 218 UI_ACTION_LONG_TIMEOUT_MS)) { 219 throw new WifiP2pManagerException( 220 "Expected connect invitation did not occur within timeout."); 221 } 222 if (!mUiDevice.wait(Until.hasObject(By.text(deviceName)), UI_ACTION_SHORT_TIMEOUT_MS)) { 223 throw new WifiP2pManagerException( 224 "The connect invitation is not triggered by expected peer device."); 225 } 226 Pattern pattern = Pattern.compile("(ACCEPT|OK|Accept)"); 227 if (!mUiDevice.wait(Until.hasObject(By.text(pattern).clazz(Button.class)), 228 UI_ACTION_SHORT_TIMEOUT_MS)) { 229 throw new WifiP2pManagerException("Accept button did not occur within timeout."); 230 } 231 UiObject2 acceptButton = mUiDevice.findObject(By.text(pattern).clazz(Button.class)); 232 if (acceptButton == null) { 233 throw new WifiP2pManagerException( 234 "There's no accept button for the connect invitation."); 235 } 236 acceptButton.click(); 237 } 238 239 /** 240 * Remove the current p2p group. 241 * 242 * @return The event posted by the callback methods of {@link ActionListener}. 243 */ 244 @Rpc(description = "Remove the current p2p group.") wifiP2pRemoveGroup()245 public Bundle wifiP2pRemoveGroup() throws Throwable { 246 checkChannel(); 247 String callbackId = UUID.randomUUID().toString(); 248 mP2pManager.removeGroup(mChannel, new ActionListener(callbackId)); 249 return waitActionListenerResult(callbackId); 250 } 251 252 /** Request the number of persistent p2p group. */ 253 @AsyncRpc(description = "Request the number of persistent p2p group") wifiP2pRequestPersistentGroupInfo(String callbackId)254 public void wifiP2pRequestPersistentGroupInfo(String callbackId) throws Throwable { 255 checkChannel(); 256 mP2pManager.requestPersistentGroupInfo( 257 mChannel, new PersistentGroupInfoListener(callbackId)); 258 } 259 260 /** 261 * Delete the persistent p2p group with the given network ID. 262 * 263 * @return The event posted by the callback methods of {@link ActionListener}. 264 */ 265 @Rpc(description = "Delete the persistent p2p group with the given network ID.") wifiP2pDeletePersistentGroup(int networkId)266 public Bundle wifiP2pDeletePersistentGroup(int networkId) throws Throwable { 267 checkChannel(); 268 String callbackId = UUID.randomUUID().toString(); 269 mP2pManager.deletePersistentGroup(mChannel, networkId, new ActionListener(callbackId)); 270 return waitActionListenerResult(callbackId); 271 } 272 273 /** 274 * Close the current P2P connection and indicate to the P2P service that connections created by 275 * the app can be removed. 276 */ 277 @Rpc( 278 description = 279 "Close the current P2P connection and indicate to the P2P service that" 280 + " connections created by the app can be removed.") p2pClose()281 public void p2pClose() { 282 if (mChannel == null) { 283 Log.d("Channel has already closed, skip WifiP2pManager.Channel.close()"); 284 return; 285 } 286 mChannel.close(); 287 mChannel = null; 288 if (mStateChangedReceiver != null) { 289 mContext.unregisterReceiver(mStateChangedReceiver); 290 mStateChangedReceiver = null; 291 } 292 } 293 294 @Override shutdown()295 public void shutdown() { 296 p2pClose(); 297 } 298 299 private class WifiP2pStateChangedReceiver extends BroadcastReceiver { 300 private String mCallbackId; 301 WifiP2pStateChangedReceiver(@onNull String callbackId)302 private WifiP2pStateChangedReceiver(@NonNull String callbackId) { 303 this.mCallbackId = callbackId; 304 } 305 306 @Override onReceive(Context mContext, Intent intent)307 public void onReceive(Context mContext, Intent intent) { 308 String action = intent.getAction(); 309 SnippetEvent event = new SnippetEvent(mCallbackId, action); 310 String logPrefix = "Got intent: action=" + action + ", "; 311 switch (action) { 312 case WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION: 313 int wifiP2pState = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0); 314 Log.d(logPrefix + "wifiP2pState=" + wifiP2pState); 315 event.getData().putInt(WifiP2pManager.EXTRA_WIFI_STATE, wifiP2pState); 316 break; 317 case WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION: 318 WifiP2pDeviceList peerList = (WifiP2pDeviceList) intent.getParcelableExtra( 319 WifiP2pManager.EXTRA_P2P_DEVICE_LIST); 320 Log.d(logPrefix + "p2pPeerList=" + peerList.toString()); 321 event.getData().putParcelableArrayList( 322 EVENT_KEY_PEER_LIST, BundleUtils.fromWifiP2pDeviceList(peerList)); 323 break; 324 case WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION: 325 NetworkInfo networkInfo = intent.getParcelableExtra( 326 WifiP2pManager.EXTRA_NETWORK_INFO); 327 WifiP2pInfo p2pInfo = (WifiP2pInfo) intent.getParcelableExtra( 328 WifiP2pManager.EXTRA_WIFI_P2P_INFO); 329 WifiP2pGroup p2pGroup = (WifiP2pGroup) intent.getParcelableExtra( 330 WifiP2pManager.EXTRA_WIFI_P2P_GROUP); 331 Log.d(logPrefix + "networkInfo=" + String.valueOf(networkInfo) + ", p2pInfo=" 332 + String.valueOf(p2pInfo) + ", p2pGroup=" + String.valueOf(p2pGroup) 333 ); 334 if (networkInfo != null) { 335 event.getData().putBoolean( 336 "isConnected", networkInfo.isConnected()); 337 } else { 338 event.getData().putBoolean("isConnected", false); 339 } 340 event.getData().putBundle( 341 EVENT_KEY_P2P_INFO, BundleUtils.fromWifiP2pInfo(p2pInfo)); 342 event.getData().putBundle( 343 EVENT_KEY_P2P_GROUP, BundleUtils.fromWifiP2pGroup(p2pGroup)); 344 break; 345 } 346 EventCache.getInstance().postEvent(event); 347 } 348 } 349 350 private static class ActionListener implements WifiP2pManager.ActionListener { 351 public static final String CALLBACK_EVENT_NAME = "WifiP2pManagerActionListenerCallback"; 352 353 private final String mCallbackId; 354 ActionListener(String callbackId)355 ActionListener(String callbackId) { 356 this.mCallbackId = callbackId; 357 } 358 359 @Override onSuccess()360 public void onSuccess() { 361 SnippetEvent event = new SnippetEvent(mCallbackId, CALLBACK_EVENT_NAME); 362 event.getData().putString(EVENT_KEY_CALLBACK_NAME, ACTION_LISTENER_ON_SUCCESS); 363 EventCache.getInstance().postEvent(event); 364 } 365 366 @Override onFailure(int reason)367 public void onFailure(int reason) { 368 SnippetEvent event = new SnippetEvent(mCallbackId, CALLBACK_EVENT_NAME); 369 event.getData().putString(EVENT_KEY_CALLBACK_NAME, ACTION_LISTENER_ON_FAILURE); 370 event.getData().putInt(EVENT_KEY_REASON, reason); 371 EventCache.getInstance().postEvent(event); 372 } 373 } 374 375 private static class DeviceInfoListener implements WifiP2pManager.DeviceInfoListener { 376 public static final String EVENT_NAME_ON_DEVICE_INFO = "WifiP2pOnDeviceInfoAvailable"; 377 378 private final String mCallbackId; 379 DeviceInfoListener(String callbackId)380 DeviceInfoListener(String callbackId) { 381 this.mCallbackId = callbackId; 382 } 383 384 @Override onDeviceInfoAvailable(WifiP2pDevice device)385 public void onDeviceInfoAvailable(WifiP2pDevice device) { 386 if (device == null) { 387 return; 388 } 389 Log.d("onDeviceInfoAvailable: " + device.toString()); 390 SnippetEvent event = new SnippetEvent(mCallbackId, EVENT_NAME_ON_DEVICE_INFO); 391 event.getData().putBundle(EVENT_KEY_P2P_DEVICE, BundleUtils.fromWifiP2pDevice(device)); 392 EventCache.getInstance().postEvent(event); 393 } 394 } 395 396 private static class WifiP2pPeerListListener implements WifiP2pManager.PeerListListener { 397 private final String mCallbackId; 398 WifiP2pPeerListListener(String callbackId)399 WifiP2pPeerListListener(String callbackId) { 400 this.mCallbackId = callbackId; 401 } 402 403 @Override onPeersAvailable(WifiP2pDeviceList newPeers)404 public void onPeersAvailable(WifiP2pDeviceList newPeers) { 405 Log.d("onPeersAvailable: " + newPeers.getDeviceList()); 406 ArrayList<Bundle> devices = BundleUtils.fromWifiP2pDeviceList(newPeers); 407 SnippetEvent event = new SnippetEvent(mCallbackId, "WifiP2pOnPeersAvailable"); 408 event.getData().putParcelableArrayList(EVENT_KEY_PEER_LIST, devices); 409 event.getData().putLong("timestampMs", System.currentTimeMillis()); 410 EventCache.getInstance().postEvent(event); 411 } 412 } 413 414 private static class PersistentGroupInfoListener implements 415 WifiP2pManager.PersistentGroupInfoListener { 416 private final String mCallbackId; 417 PersistentGroupInfoListener(String callbackId)418 PersistentGroupInfoListener(String callbackId) { 419 this.mCallbackId = callbackId; 420 } 421 422 @Override onPersistentGroupInfoAvailable(@onNull WifiP2pGroupList groups)423 public void onPersistentGroupInfoAvailable(@NonNull WifiP2pGroupList groups) { 424 Log.d("onPersistentGroupInfoAvailable: " + groups.toString()); 425 SnippetEvent event = new SnippetEvent(mCallbackId, "onPersistentGroupInfoAvailable"); 426 event.getData().putParcelableArrayList( 427 "groupList", BundleUtils.fromWifiP2pGroupList(groups)); 428 EventCache.getInstance().postEvent(event); 429 } 430 } 431 checkChannel()432 private void checkChannel() throws WifiP2pManagerException { 433 if (mChannel == null) { 434 throw new WifiP2pManagerException( 435 "Channel is not created, please call 'wifiP2pInitialize' first."); 436 } 437 } 438 checkP2pManager()439 private void checkP2pManager() throws WifiP2pManagerException { 440 if (mP2pManager == null) { 441 throw new WifiP2pManagerException("Device does not support Wi-Fi Direct."); 442 } 443 } 444 checkPermissions(Context context, String... permissions)445 private static void checkPermissions(Context context, String... permissions) { 446 for (String permission : permissions) { 447 if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { 448 throw new SecurityException( 449 "Permission denied (missing " + permission + " permission)"); 450 } 451 } 452 } 453 454 /** Wait until any callback of {@link ActionListener} is triggered. */ waitActionListenerResult(String callbackId)455 private Bundle waitActionListenerResult(String callbackId) throws Throwable { 456 SnippetEvent event = waitForSnippetEvent( 457 callbackId, ActionListener.CALLBACK_EVENT_NAME, TIMEOUT_SHORT_MS); 458 Log.d("Got action listener result event: " + event.getData().toString()); 459 return event.getData(); 460 } 461 462 /** Wait until any callback of {@link ActionListener} is triggered and verify it succeeded. */ verifyActionListenerSucceed(String callbackId)463 private void verifyActionListenerSucceed(String callbackId) throws Throwable { 464 Bundle eventData = waitActionListenerResult(callbackId); 465 String result = eventData.getString(EVENT_KEY_CALLBACK_NAME); 466 if (result == ACTION_LISTENER_ON_SUCCESS) { 467 return; 468 } 469 if (result == ACTION_LISTENER_ON_FAILURE) { 470 throw new WifiP2pManagerException( 471 "Action failed with reason code: " + eventData.getInt(EVENT_KEY_REASON) 472 ); 473 } 474 throw new WifiP2pManagerException("Action got unknown event: " + eventData.toString()); 475 } 476 waitForSnippetEvent( String callbackId, String eventName, Integer timeout)477 private static SnippetEvent waitForSnippetEvent( 478 String callbackId, String eventName, Integer timeout) throws Throwable { 479 String qId = EventCache.getQueueId(callbackId, eventName); 480 LinkedBlockingDeque<SnippetEvent> q = EventCache.getInstance().getEventDeque(qId); 481 SnippetEvent result; 482 try { 483 result = q.pollFirst(timeout, TimeUnit.MILLISECONDS); 484 } catch (InterruptedException e) { 485 throw e.getCause(); 486 } 487 488 if (result == null) { 489 throw new TimeoutException( 490 "Timed out waiting(" + timeout + " millis) for SnippetEvent: " + callbackId); 491 } 492 return result; 493 } 494 } 495