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