1 /*
2  * 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.wifi.aware;
18 
19 import android.Manifest;
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;
25 import android.net.MacAddress;
26 import android.net.wifi.aware.AttachCallback;
27 import android.net.wifi.aware.Characteristics;
28 import android.net.wifi.aware.DiscoverySession;
29 import android.net.wifi.aware.DiscoverySessionCallback;
30 import android.net.wifi.aware.IdentityChangedListener;
31 import android.net.wifi.aware.PeerHandle;
32 import android.net.wifi.aware.PublishConfig;
33 import android.net.wifi.aware.PublishDiscoverySession;
34 import android.net.wifi.aware.ServiceDiscoveryInfo;
35 import android.net.wifi.aware.SubscribeConfig;
36 import android.net.wifi.aware.SubscribeDiscoverySession;
37 import android.net.wifi.aware.WifiAwareManager;
38 import android.net.wifi.aware.WifiAwareNetworkSpecifier;
39 import android.net.wifi.aware.WifiAwareSession;
40 import android.net.wifi.rtt.RangingRequest;
41 import android.net.wifi.rtt.RangingResult;
42 import android.net.wifi.rtt.RangingResultCallback;
43 import android.net.wifi.rtt.WifiRttManager;
44 import android.os.Bundle;
45 import android.os.Handler;
46 import android.os.HandlerThread;
47 
48 import androidx.annotation.NonNull;
49 import androidx.test.core.app.ApplicationProvider;
50 
51 import com.google.android.mobly.snippet.Snippet;
52 import com.google.android.mobly.snippet.event.EventCache;
53 import com.google.android.mobly.snippet.event.SnippetEvent;
54 import com.google.android.mobly.snippet.rpc.AsyncRpc;
55 import com.google.android.mobly.snippet.rpc.Rpc;
56 import com.google.android.mobly.snippet.rpc.RpcOptional;
57 import com.google.android.mobly.snippet.util.Log;
58 
59 import org.json.JSONException;
60 import org.json.JSONObject;
61 
62 import java.nio.charset.StandardCharsets;
63 import java.util.List;
64 import java.util.concurrent.ConcurrentHashMap;
65 
66 /**
67  * Snippet class for exposing {@link WifiAwareManager} APIs.
68  */
69 public class WifiAwareManagerSnippet implements Snippet {
70     private final Context mContext;
71     private final WifiAwareManager mWifiAwareManager;
72     private final WifiRttManager mWifiRttManager;
73     private final Handler mHandler;
74     // WifiAwareSession will be initialized after attach.
75     private final ConcurrentHashMap<String, WifiAwareSession> mAttachSessions =
76             new ConcurrentHashMap<>();
77     // DiscoverySession will be initialized after publish or subscribe
78     private final ConcurrentHashMap<String, DiscoverySession> mDiscoverySessions =
79             new ConcurrentHashMap<>();
80     private final ConcurrentHashMap<Integer, PeerHandle> mPeerHandles = new ConcurrentHashMap<>();
81     private final EventCache eventCache = EventCache.getInstance();
82     private WifiAwareStateChangedReceiver stateChangedReceiver;
83 
84     /**
85      * Custom exception class for handling specific errors related to the WifiAwareManagerSnippet
86      * operations.
87      */
88     private static class WifiAwareManagerSnippetException extends Exception {
WifiAwareManagerSnippetException(String msg)89         WifiAwareManagerSnippetException(String msg) {
90             super(msg);
91         }
92     }
93 
WifiAwareManagerSnippet()94     public WifiAwareManagerSnippet() throws WifiAwareManagerSnippetException {
95         mContext = ApplicationProvider.getApplicationContext();
96         PermissionUtils.checkPermissions(mContext, Manifest.permission.ACCESS_WIFI_STATE,
97                 Manifest.permission.CHANGE_WIFI_STATE, Manifest.permission.ACCESS_FINE_LOCATION,
98                 Manifest.permission.NEARBY_WIFI_DEVICES
99         );
100         mWifiAwareManager = mContext.getSystemService(WifiAwareManager.class);
101         checkWifiAwareManager();
102         mWifiRttManager = mContext.getSystemService(WifiRttManager.class);
103         HandlerThread handlerThread = new HandlerThread("Snippet-Aware");
104         handlerThread.start();
105         mHandler = new Handler(handlerThread.getLooper());
106     }
107 
108     /**
109      * Returns whether Wi-Fi Aware is supported.
110      */
111     @Rpc(description = "Is Wi-Fi Aware supported.")
wifiAwareIsSupported()112     public boolean wifiAwareIsSupported() {
113         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
114     }
115 
116     /**
117      * Returns whether Wi-Fi RTT is supported.
118      */
119     @Rpc(description = "Is Wi-Fi RTT supported.")
wifiAwareIsRttSupported()120     public boolean wifiAwareIsRttSupported() {
121         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT);
122     }
123 
124     /**
125      * Use {@link WifiAwareManager#attach(AttachCallback, Handler)} to attach to the Wi-Fi Aware.
126      *
127      * @param callbackId Assigned automatically by mobly. Also will be used as Attach session id for
128      *                   further operations
129      */
130     @AsyncRpc(
131             description = "Attach to the Wi-Fi Aware service - enabling the application to "
132                     + "create discovery sessions or publish or subscribe to services."
133     )
wifiAwareAttach(String callbackId)134     public void wifiAwareAttach(String callbackId) {
135         attach(callbackId, false);
136     }
137 
138     /**
139      * Use {@link WifiAwareManager#attach(AttachCallback, Handler)} to attach to the Wi-Fi Aware.
140      *
141      * @param callbackId Assigned automatically by mobly. Also will be used as Attach session id for
142      *                   further operations
143      * @param identityCb If true, the application will be notified of changes to the device's
144      */
145     @AsyncRpc(
146             description = "Attach to the Wi-Fi Aware service - enabling the application to "
147                     + "create discovery sessions or publish or subscribe to services."
148     )
wifiAwareAttached(String callbackId, boolean identityCb)149     public void wifiAwareAttached(String callbackId, boolean identityCb)
150             throws WifiAwareManagerSnippetException {
151         attach(callbackId, identityCb);
152     }
153 
attach(String callbackId, boolean identityCb)154     private void attach(String callbackId, boolean identityCb) {
155         AttachCallback attachCallback = new AttachCallback() {
156             @Override
157             public void onAttachFailed() {
158                 super.onAttachFailed();
159                 sendEvent(callbackId, "onAttachFailed");
160             }
161 
162             @Override
163             public void onAttached(WifiAwareSession session) {
164                 super.onAttached(session);
165                 mAttachSessions.put(callbackId, session);
166                 sendEvent(callbackId, "onAttached");
167 
168             }
169 
170             @Override
171             public void onAwareSessionTerminated() {
172                 super.onAwareSessionTerminated();
173                 mAttachSessions.remove(callbackId);
174                 sendEvent(callbackId, "onAwareSessionTerminated");
175             }
176         };
177         if (identityCb) {
178             mWifiAwareManager.attach(attachCallback,
179                     new AwareIdentityChangeListenerPostsEvents(eventCache, callbackId), mHandler
180             );
181         } else {
182             mWifiAwareManager.attach(attachCallback, mHandler);
183         }
184 
185     }
186 
187     private static class AwareIdentityChangeListenerPostsEvents extends IdentityChangedListener {
188         private final EventCache eventCache;
189         private final String callbackId;
190 
AwareIdentityChangeListenerPostsEvents(EventCache eventCache, String callbackId)191         public AwareIdentityChangeListenerPostsEvents(EventCache eventCache, String callbackId) {
192             this.eventCache = eventCache;
193             this.callbackId = callbackId;
194         }
195 
196         @Override
onIdentityChanged(byte[] mac)197         public void onIdentityChanged(byte[] mac) {
198             SnippetEvent event = new SnippetEvent(callbackId, "WifiAwareAttachOnIdentityChanged");
199             event.getData().putLong("timestampMs", System.currentTimeMillis());
200             event.getData().putString("mac", MacAddress.fromBytes(mac).toString());
201             eventCache.postEvent(event);
202             Log.d("WifiAwareattach identity changed called for WifiAwareAttachOnIdentityChanged");
203         }
204     }
205 
206     /**
207      * Starts listening for wifiAware state change related broadcasts.
208      *
209      * @param callbackId the callback id
210      */
211     @AsyncRpc(description = "Start listening for wifiAware state change related broadcasts.")
wifiAwareMonitorStateChange(String callbackId)212     public void wifiAwareMonitorStateChange(String callbackId) {
213         stateChangedReceiver = new WifiAwareStateChangedReceiver(eventCache, callbackId);
214         IntentFilter filter = new IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED);
215         mContext.registerReceiver(stateChangedReceiver, filter);
216     }
217 
218     /**
219      * Stops listening for wifiAware state change related broadcasts.
220      */
221     @Rpc(description = "Stop listening for wifiAware state change related broadcasts.")
wifiAwareMonitorStopStateChange()222     public void wifiAwareMonitorStopStateChange() {
223         if (stateChangedReceiver != null) {
224             mContext.unregisterReceiver(stateChangedReceiver);
225             stateChangedReceiver = null;
226         }
227     }
228 
229     class WifiAwareStateChangedReceiver extends BroadcastReceiver {
230         private final EventCache eventCache;
231         private final String callbackId;
232 
WifiAwareStateChangedReceiver(EventCache eventCache, String callbackId)233         public WifiAwareStateChangedReceiver(EventCache eventCache, String callbackId) {
234             this.eventCache = eventCache;
235             this.callbackId = callbackId;
236         }
237 
238         @Override
onReceive(Context c, Intent intent)239         public void onReceive(Context c, Intent intent) {
240             boolean isAvailable = mWifiAwareManager.isAvailable();
241             SnippetEvent event = new SnippetEvent(callbackId,
242                     "WifiAwareState" + (isAvailable ? "Available" : "NotAvailable")
243             );
244             eventCache.postEvent(event);
245         }
246     }
247 
248     /**
249      * Use {@link WifiAwareSession#close()} to detach from the Wi-Fi Aware.
250      *
251      * @param sessionId The Id of the Aware attach session
252      */
253     @Rpc(description = "Detach from the Wi-Fi Aware service.")
wifiAwareDetach(String sessionId)254     public void wifiAwareDetach(String sessionId) {
255         WifiAwareSession session = mAttachSessions.remove(sessionId);
256         if (session != null) {
257             session.close();
258         }
259 
260     }
261 
262     /**
263      * Check if Wi-Fi Aware is attached.
264      *
265      * @param sessionId The Id of the Aware attached event callback id
266      */
267     @Rpc(description = "Check if Wi-Fi aware is attached")
wifiAwareIsSessionAttached(String sessionId)268     public boolean wifiAwareIsSessionAttached(String sessionId) {
269         return !mAttachSessions.isEmpty() && mAttachSessions.containsKey(sessionId);
270     }
271 
272     /**
273      * Check if Wi-Fi Aware is  pairing supported.
274      */
275     @Rpc(description = "Check if Wi-Fi aware pairing is available")
wifiAwareIsAwarePairingSupported()276     public Boolean wifiAwareIsAwarePairingSupported() throws WifiAwareManagerSnippetException {
277         checkWifiAwareManager();
278         Characteristics characteristics = mWifiAwareManager.getCharacteristics();
279         if (characteristics == null) {
280             throw new WifiAwareManagerSnippetException(
281                     "Can not get Wi-Fi Aware characteristics. Possible reasons include: 1. The "
282                             + "Wi-Fi Aware service is not initialized. Please call "
283                             + "attachWifiAware first. 2. The device does not support Wi-Fi Aware."
284                             + " Check the device's hardware and driver Wi-Fi Aware support.");
285 
286         }
287         return characteristics.isAwarePairingSupported();
288     }
289 
290 
291     /**
292      * Check if Wi-Fi Aware services is available.
293      */
checkWifiAwareManager()294     private void checkWifiAwareManager() throws WifiAwareManagerSnippetException {
295         if (mWifiAwareManager == null) {
296             throw new WifiAwareManagerSnippetException("Device does not support Wi-Fi Aware.");
297         }
298     }
299 
300     /**
301      * Checks if Wi-Fi RTT Manager has been set.
302      */
checkWifiRttManager()303     private void checkWifiRttManager() throws WifiAwareManagerSnippetException {
304         if (mWifiRttManager == null) {
305             throw new WifiAwareManagerSnippetException("Device does not support Wi-Fi Rtt.");
306         }
307     }
308 
309     /**
310      * Checks if Wi-Fi RTT is available.
311      */
checkWifiRttAvailable()312     private void checkWifiRttAvailable() throws WifiAwareManagerSnippetException {
313         if (!mWifiRttManager.isAvailable()) {
314             throw new WifiAwareManagerSnippetException("WiFi RTT is not available now.");
315         }
316     }
317 
318     /**
319      * Check if Wi-Fi Aware is available.
320      */
321     @Rpc(description = "Check if Wi-Fi Aware is available")
wifiAwareIsAvailable()322     public Boolean wifiAwareIsAvailable() {
323         return mWifiAwareManager.isAvailable();
324     }
325 
326     /**
327      * Send callback event of current method
328      */
sendEvent(String callbackId, String methodName)329     private void sendEvent(String callbackId, String methodName) {
330         SnippetEvent event = new SnippetEvent(callbackId, methodName);
331         EventCache.getInstance().postEvent(event);
332     }
333 
334     class WifiAwareDiscoverySessionCallback extends DiscoverySessionCallback {
335 
336         String mCallBackId = "";
337 
WifiAwareDiscoverySessionCallback(String callBackId)338         WifiAwareDiscoverySessionCallback(String callBackId) {
339             this.mCallBackId = callBackId;
340         }
341 
putMatchFilterData(List<byte[]> matchFilter, SnippetEvent event)342         private void putMatchFilterData(List<byte[]> matchFilter, SnippetEvent event) {
343             Bundle[] matchFilterBundle = new Bundle[matchFilter.size()];
344             int index = 0;
345             for (byte[] filter : matchFilter) {
346                 Bundle bundle = new Bundle();
347                 bundle.putByteArray("value", filter);
348                 matchFilterBundle[index] = bundle;
349                 index++;
350             }
351             event.getData().putParcelableArray("matchFilter", matchFilterBundle);
352         }
353 
354         @Override
onPublishStarted(PublishDiscoverySession session)355         public void onPublishStarted(PublishDiscoverySession session) {
356             mDiscoverySessions.put(mCallBackId, session);
357             SnippetEvent snippetEvent = new SnippetEvent(mCallBackId, "discoveryResult");
358             snippetEvent.getData().putString("callbackName", "onPublishStarted");
359             snippetEvent.getData().putBoolean("isSessionInitialized", session != null);
360             EventCache.getInstance().postEvent(snippetEvent);
361         }
362 
363         @Override
onSubscribeStarted(SubscribeDiscoverySession session)364         public void onSubscribeStarted(SubscribeDiscoverySession session) {
365             mDiscoverySessions.put(mCallBackId, session);
366             SnippetEvent snippetEvent = new SnippetEvent(mCallBackId, "discoveryResult");
367             snippetEvent.getData().putString("callbackName", "onSubscribeStarted");
368             snippetEvent.getData().putBoolean("isSessionInitialized", session != null);
369             EventCache.getInstance().postEvent(snippetEvent);
370         }
371 
372         @Override
onSessionConfigUpdated()373         public void onSessionConfigUpdated() {
374             sendEvent(mCallBackId, "onSessionConfigUpdated");
375         }
376 
377         @Override
onSessionConfigFailed()378         public void onSessionConfigFailed() {
379             sendEvent(mCallBackId, "onSessionConfigFailed");
380         }
381 
382         @Override
onSessionTerminated()383         public void onSessionTerminated() {
384             sendEvent(mCallBackId, "onSessionTerminated");
385         }
386 
387         @Override
onServiceDiscovered(ServiceDiscoveryInfo info)388         public void onServiceDiscovered(ServiceDiscoveryInfo info) {
389             mPeerHandles.put(info.getPeerHandle().hashCode(), info.getPeerHandle());
390             SnippetEvent event = new SnippetEvent(mCallBackId, "onServiceDiscovered");
391             event.getData().putByteArray("serviceSpecificInfo", info.getServiceSpecificInfo());
392             event.getData().putString("pairedAlias", info.getPairedAlias());
393             event.getData().putInt("peerId", info.getPeerHandle().hashCode());
394             List<byte[]> matchFilter = info.getMatchFilters();
395             putMatchFilterData(matchFilter, event);
396             EventCache.getInstance().postEvent(event);
397         }
398 
399         @Override
onServiceDiscoveredWithinRange( PeerHandle peerHandle, byte[] serviceSpecificInfo, List<byte[]> matchFilter, int distanceMm )400         public void onServiceDiscoveredWithinRange(
401                 PeerHandle peerHandle, byte[] serviceSpecificInfo, List<byte[]> matchFilter,
402                 int distanceMm
403         ) {
404             mPeerHandles.put(peerHandle.hashCode(), peerHandle);
405             SnippetEvent event = new SnippetEvent(mCallBackId, "onServiceDiscoveredWithinRange");
406             event.getData().putByteArray("serviceSpecificInfo", serviceSpecificInfo);
407             event.getData().putInt("distanceMm", distanceMm);
408             event.getData().putInt("peerId", peerHandle.hashCode());
409             putMatchFilterData(matchFilter, event);
410             EventCache.getInstance().postEvent(event);
411         }
412 
413         @Override
onMessageSendSucceeded(int messageId)414         public void onMessageSendSucceeded(int messageId) {
415             SnippetEvent event = new SnippetEvent(mCallBackId, "messageSendResult");
416             event.getData().putString("callbackName", "onMessageSendSucceeded");
417             event.getData().putInt("messageId", messageId);
418             EventCache.getInstance().postEvent(event);
419         }
420 
421         @Override
onMessageSendFailed(int messageId)422         public void onMessageSendFailed(int messageId) {
423             SnippetEvent event = new SnippetEvent(mCallBackId, "messageSendResult");
424             event.getData().putString("callbackName", "onMessageSendFailed");
425             event.getData().putInt("messageId", messageId);
426             EventCache.getInstance().postEvent(event);
427         }
428 
429         @Override
onMessageReceived(PeerHandle peerHandle, byte[] message)430         public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
431             mPeerHandles.put(peerHandle.hashCode(), peerHandle);
432             SnippetEvent event = new SnippetEvent(mCallBackId, "onMessageReceived");
433             event.getData().putByteArray("receivedMessage", message);
434             event.getData().putInt("peerId", peerHandle.hashCode());
435             EventCache.getInstance().postEvent(event);
436         }
437 
438         @Override
onPairingSetupRequestReceived(PeerHandle peerHandle, int requestId)439         public void onPairingSetupRequestReceived(PeerHandle peerHandle, int requestId) {
440             SnippetEvent event = new SnippetEvent(mCallBackId, "onPairingSetupRequestReceived");
441             event.getData().putInt("pairingRequestId", requestId);
442             event.getData().putInt("peerId", peerHandle.hashCode());
443             EventCache.getInstance().postEvent(event);
444         }
445 
446         @Override
onPairingSetupSucceeded(PeerHandle peerHandle, String alias)447         public void onPairingSetupSucceeded(PeerHandle peerHandle, String alias) {
448             SnippetEvent event = new SnippetEvent(mCallBackId, "onPairingSetupSucceeded");
449             event.getData().putString("pairedAlias", alias);
450             event.getData().putInt("peerId", peerHandle.hashCode());
451             EventCache.getInstance().postEvent(event);
452         }
453 
454         @Override
onPairingSetupFailed(PeerHandle peerHandle)455         public void onPairingSetupFailed(PeerHandle peerHandle) {
456             SnippetEvent event = new SnippetEvent(mCallBackId, "onPairingSetupFailed");
457             event.getData().putInt("peerId", peerHandle.hashCode());
458             EventCache.getInstance().postEvent(event);
459         }
460 
461         @Override
onPairingVerificationSucceed( @onNull PeerHandle peerHandle, @NonNull String alias )462         public void onPairingVerificationSucceed(
463                 @NonNull PeerHandle peerHandle, @NonNull String alias
464         ) {
465             super.onPairingVerificationSucceed(peerHandle, alias);
466             SnippetEvent event = new SnippetEvent(mCallBackId, "onPairingVerificationSucceed");
467             event.getData().putString("pairedAlias", alias);
468             event.getData().putInt("peerId", peerHandle.hashCode());
469             EventCache.getInstance().postEvent(event);
470         }
471 
472         @Override
onPairingVerificationFailed(PeerHandle peerHandle)473         public void onPairingVerificationFailed(PeerHandle peerHandle) {
474             SnippetEvent event = new SnippetEvent(mCallBackId, "onPairingVerificationFailed");
475             event.getData().putInt("peerId", peerHandle.hashCode());
476             EventCache.getInstance().postEvent(event);
477         }
478 
479         @Override
onBootstrappingSucceeded(PeerHandle peerHandle, int method)480         public void onBootstrappingSucceeded(PeerHandle peerHandle, int method) {
481             SnippetEvent event = new SnippetEvent(mCallBackId, "onBootstrappingSucceeded");
482             event.getData().putInt("bootstrappingMethod", method);
483             event.getData().putInt("peerId", peerHandle.hashCode());
484             EventCache.getInstance().postEvent(event);
485         }
486 
487         @Override
onBootstrappingFailed(PeerHandle peerHandle)488         public void onBootstrappingFailed(PeerHandle peerHandle) {
489             SnippetEvent event = new SnippetEvent(mCallBackId, "onBootstrappingFailed");
490             event.getData().putInt("peerId", peerHandle.hashCode());
491             EventCache.getInstance().postEvent(event);
492         }
493 
494         @Override
onServiceLost(PeerHandle peerHandle, int reason)495         public void onServiceLost(PeerHandle peerHandle, int reason) {
496             SnippetEvent event = new SnippetEvent(mCallBackId, "WifiAwareSessionOnServiceLost");
497             event.getData().putString("discoverySessionId", mCallBackId);
498             event.getData().putInt("peerId", peerHandle.hashCode());
499             event.getData().putInt("lostReason", reason);
500             EventCache.getInstance().postEvent(event);
501         }
502     }
503 
getWifiAwareSession(String sessionId)504     private WifiAwareSession getWifiAwareSession(String sessionId)
505             throws WifiAwareManagerSnippetException {
506         WifiAwareSession session = mAttachSessions.get(sessionId);
507         if (session == null) {
508             throw new WifiAwareManagerSnippetException(
509                     "Wi-Fi Aware session is not attached. Please call wifiAwareAttach first.");
510         }
511         return session;
512     }
513 
514 
515     /**
516      * Creates a new Aware subscribe discovery session. For Android T and later, this method
517      * requires NEARBY_WIFI_DEVICES permission and user permission flag "neverForLocation". For
518      * earlier versions, this method requires NEARBY_WIFI_DEVICES and ACCESS_FINE_LOCATION
519      * permissions.
520      *
521      * @param sessionId       The Id of the Aware attach session, should be the callbackId from
522      *                        {@link #wifiAwareAttach(String)}
523      * @param callbackId      Assigned automatically by mobly. Also will be used as discovery
524      *                        session id for further operations
525      * @param subscribeConfig Defines the subscription configuration via WifiAwareJsonDeserializer.
526      */
527     @AsyncRpc(
528             description = "Create a Wi-Fi Aware subscribe discovery session and handle callbacks."
529     )
wifiAwareSubscribe( String callbackId, String sessionId, SubscribeConfig subscribeConfig )530     public void wifiAwareSubscribe(
531             String callbackId, String sessionId, SubscribeConfig subscribeConfig
532     ) throws JSONException, WifiAwareManagerSnippetException {
533         WifiAwareSession session = getWifiAwareSession(sessionId);
534         Log.v("Creating a new Aware subscribe session with config: " + subscribeConfig.toString());
535         WifiAwareDiscoverySessionCallback myDiscoverySessionCallback =
536                 new WifiAwareDiscoverySessionCallback(callbackId);
537         session.subscribe(subscribeConfig, myDiscoverySessionCallback, mHandler);
538     }
539 
540     /**
541      * Creates a new Aware publish discovery session. Requires NEARBY_WIFI_DEVICES (with
542      * neverForLocation) or ACCESS_FINE_LOCATION for Android TIRAMISU+. ACCESS_FINE_LOCATION is
543      * required for earlier versions.
544      *
545      * @param sessionId     The Id of the Aware attach session, should be the callbackId from
546      *                      {@link #wifiAwareAttach(String)}
547      * @param callbackId    Assigned automatically by mobly. Also will be used as discovery session
548      *                      id for further operations
549      * @param publishConfig Defines the publish configuration via WifiAwareJsonDeserializer.
550      */
551     @AsyncRpc(description = "Create a Wi-Fi Aware publish discovery session and handle callbacks.")
wifiAwarePublish(String callbackId, String sessionId, PublishConfig publishConfig)552     public void wifiAwarePublish(String callbackId, String sessionId, PublishConfig publishConfig)
553             throws JSONException, WifiAwareManagerSnippetException {
554         WifiAwareSession session = getWifiAwareSession(sessionId);
555         Log.v("Creating a new Aware publish session with config: " + publishConfig.toString());
556         WifiAwareDiscoverySessionCallback myDiscoverySessionCallback =
557                 new WifiAwareDiscoverySessionCallback(callbackId);
558         session.publish(publishConfig, myDiscoverySessionCallback, mHandler);
559     }
560 
getPeerHandler(int peerId)561     private PeerHandle getPeerHandler(int peerId) throws WifiAwareManagerSnippetException {
562         PeerHandle handle = mPeerHandles.get(peerId);
563         if (handle == null) {
564             throw new WifiAwareManagerSnippetException(
565                     "GetPeerHandler failed. Please call publish or subscribe method, error "
566                             + "peerId: " + peerId + ", mPeerHandles: " + mPeerHandles);
567         }
568         return handle;
569     }
570 
getDiscoverySession(String discoverySessionId)571     private DiscoverySession getDiscoverySession(String discoverySessionId)
572             throws WifiAwareManagerSnippetException {
573         DiscoverySession session = mDiscoverySessions.get(discoverySessionId);
574         if (session == null) {
575             throw new WifiAwareManagerSnippetException(
576                     "GetDiscoverySession failed. Please call publish or subscribe method, "
577                             + "error discoverySessionId: " + discoverySessionId
578                             + ", mDiscoverySessions: " + mDiscoverySessions);
579         }
580         return session;
581 
582     }
583 
584     /**
585      * Sends a message to a peer using Wi-Fi Aware.
586      *
587      * <p>This method sends a specified message to a peer device identified by a peer handle
588      * in an ongoing Wi-Fi Aware discovery session. The message is sent asynchronously, and the
589      * method waits for the send status to confirm whether the message was successfully sent or if
590      * any errors occurred.</p>
591      *
592      * <p>Before sending the message, this method checks if there is an active discovery
593      * session. If there is no active session, it throws a
594      * {@link WifiAwareManagerSnippetException}.</p>
595      *
596      * @param discoverySessionId The Id of the discovery session, should be the callbackId from
597      *                           publish/subscribe action
598      * @param peerId             identifier for the peer handle
599      * @param messageId          an integer representing the message ID, which is used to track the
600      *                           message.
601      * @param message            a {@link String} containing the message to be sent.
602      * @throws WifiAwareManagerSnippetException if there is no active discovery session or if
603      *                                          sending the message fails.
604      * @see android.net.wifi.aware.DiscoverySession#sendMessage
605      * @see android.net.wifi.aware.PeerHandle
606      * @see java.nio.charset.StandardCharsets#UTF_8
607      */
608     @Rpc(description = "Send a message to a peer using Wi-Fi Aware.")
wifiAwareSendMessage( String discoverySessionId, int peerId, int messageId, String message )609     public void wifiAwareSendMessage(
610             String discoverySessionId, int peerId, int messageId, String message
611     ) throws WifiAwareManagerSnippetException {
612         // 4. send message & wait for send status
613         DiscoverySession session = getDiscoverySession(discoverySessionId);
614         PeerHandle handle = getPeerHandler(peerId);
615         session.sendMessage(handle, messageId, message.getBytes(StandardCharsets.UTF_8));
616     }
617 
618     /**
619      * Closes the current Wi-Fi Aware discovery session if it is active.
620      *
621      * <p>This method checks if there is an active discovery session. If so,
622      * it closes the session and sets the session object to null. This ensures that resources are
623      * properly released and the session is cleanly terminated.</p>
624      *
625      * @param discoverySessionId The Id of the discovery session
626      */
627     @Rpc(description = "Close the current Wi-Fi Aware discovery session.")
wifiAwareCloseDiscoverSession(String discoverySessionId)628     public void wifiAwareCloseDiscoverSession(String discoverySessionId) {
629         DiscoverySession session = mDiscoverySessions.remove(discoverySessionId);
630         if (session != null) {
631             session.close();
632         }
633     }
634 
635     /**
636      * Closes all Wi-Fi Aware session if it is active. And clear all cache sessions
637      */
638     @Rpc(description = "Close the current Wi-Fi Aware session.")
wifiAwareCloseAllWifiAwareSession()639     public void wifiAwareCloseAllWifiAwareSession() {
640         for (WifiAwareSession session : mAttachSessions.values()) {
641             session.close();
642         }
643         mAttachSessions.clear();
644         mDiscoverySessions.clear();
645         mPeerHandles.clear();
646     }
647 
648     /**
649      * Creates a Wi-Fi Aware network specifier for requesting network through connectivityManager.
650      *
651      * @param discoverySessionId The Id of the discovery session,
652      * @param peerId             The Id of the peer handle
653      * @param isAcceptAnyPeer    A boolean value indicating whether the network specifier should
654      * @return a {@link String} containing the network specifier encoded as a Base64 string.
655      * @throws JSONException                    if there is an error parsing the JSON object.
656      * @throws WifiAwareManagerSnippetException if there is an error creating the network
657      *                                          specifier.
658      */
659     @Rpc(
660             description = "Create a network specifier to be used when specifying a Aware network "
661                     + "request"
662     )
wifiAwareCreateNetworkSpecifier( String discoverySessionId, int peerId, boolean isAcceptAnyPeer, @RpcOptional JSONObject jsonObject )663     public String wifiAwareCreateNetworkSpecifier(
664             String discoverySessionId, int peerId, boolean isAcceptAnyPeer,
665             @RpcOptional JSONObject jsonObject
666     ) throws JSONException, WifiAwareManagerSnippetException {
667         DiscoverySession session = getDiscoverySession(discoverySessionId);
668         PeerHandle handle = getPeerHandler(peerId);
669         WifiAwareNetworkSpecifier.Builder builder;
670         if (isAcceptAnyPeer) {
671             builder = new WifiAwareNetworkSpecifier.Builder((PublishDiscoverySession) session);
672         } else {
673             builder = new WifiAwareNetworkSpecifier.Builder(session, handle);
674         }
675         WifiAwareNetworkSpecifier specifier =
676                 WifiAwareJsonDeserializer.jsonToNetworkSpecifier(jsonObject, builder);
677         return SerializationUtil.parcelableToString(specifier);
678     }
679 
680     @Override
shutdown()681     public void shutdown() throws Exception {
682         wifiAwareCloseAllWifiAwareSession();
683     }
684 
685     /**
686      * Returns the characteristics of the WiFi Aware interface.
687      *
688      * @return WiFi Aware characteristics
689      */
690     @Rpc(description = "Get the characteristics of the WiFi Aware interface.")
getCharacteristics()691     public Characteristics getCharacteristics() {
692         return mWifiAwareManager.getCharacteristics();
693     }
694 
695     /**
696      * Creates a wifiAwareUpdatePublish discovery session. Requires NEARBY_WIFI_DEVICES (with
697      * neverForLocation) or ACCESS_FINE_LOCATION for Android TIRAMISU+. ACCESS_FINE_LOCATION is
698      * required for earlier versions.
699      *
700      * @param sessionId     The Id of the Aware attach session, should be the callbackId from
701      *                      {@link #wifiAwareAttach(String)}
702      * @param publishConfig Defines the publish configuration via WifiAwareJsonDeserializer.
703      */
704     @Rpc(description = "Create a wifiAwareUpdatePublish discovery session and handle callbacks.")
wifiAwareUpdatePublish(String sessionId, PublishConfig publishConfig)705     public void wifiAwareUpdatePublish(String sessionId, PublishConfig publishConfig)
706             throws JSONException, WifiAwareManagerSnippetException, IllegalArgumentException {
707         DiscoverySession session = getDiscoverySession(sessionId);
708         if (session == null) {
709             throw new IllegalStateException(
710                     "Calling wifiAwareUpdatePublish before session (session ID " + sessionId
711                             + ") is ready");
712         }
713         if (!(session instanceof PublishDiscoverySession)) {
714             throw new IllegalArgumentException(
715                     "Calling wifiAwareUpdatePublish with a subscribe session ID");
716         }
717         Log.v("Updating a  Aware publish session with config: " + publishConfig.toString());
718 
719         ((PublishDiscoverySession) session).updatePublish(publishConfig);
720     }
721 
722     /**
723      * Creates a wifiAwareUpdateSubscribe discovery session. For Android T and later, this method
724      * requires NEARBY_WIFI_DEVICES permission and user permission flag "neverForLocation". For
725      * earlier versions, this method requires NEARBY_WIFI_DEVICES and ACCESS_FINE_LOCATION
726      * permissions.
727      *
728      * @param sessionId       The Id of the Aware attach session, should be the callbackId from
729      *                        {@link #wifiAwareAttach(String)}
730      * @param subscribeConfig Defines the subscription configuration via WifiAwareJsonDeserializer.
731      */
732     @Rpc(description = "Create a wifiAwareUpdateSubscribe discovery session and handle callbacks.")
wifiAwareUpdateSubscribe( String sessionId, SubscribeConfig subscribeConfig )733     public void wifiAwareUpdateSubscribe(
734             String sessionId, SubscribeConfig subscribeConfig
735     ) throws JSONException, WifiAwareManagerSnippetException {
736         DiscoverySession session = getDiscoverySession(sessionId);
737         if (session == null) {
738             throw new IllegalStateException(
739                     "Calling wifiAwareUpdateSubscribe before session (session ID " + sessionId
740                             + ") is ready");
741         }
742         if (!(session instanceof SubscribeDiscoverySession)) {
743             throw new IllegalArgumentException(
744                     "Calling wifiAwareUpdateSubscribe with a publish session ID");
745         }
746         Log.v("Creating a wifiAwareUpdateSubscribe session with config: "
747                 + subscribeConfig.toString());
748         ((SubscribeDiscoverySession) session).updateSubscribe(subscribeConfig);
749 
750     }
751 
752     /**
753      * Starts Wi-Fi RTT ranging with Wi-Fi Aware peers.
754      *
755      * @param callbackId        Assigned automatically by mobly for all async RPCs.
756      * @param requestJsonObject The ranging request in JSONObject type for calling {@link
757      *                          android.net.wifi.rtt.WifiRttManager#startRanging startRanging}.
758      */
759     @AsyncRpc(description = "Start Wi-Fi RTT ranging with Wi-Fi Aware peers.")
wifiAwareStartRanging( String callbackId, JSONObject requestJsonObject )760     public void wifiAwareStartRanging(
761             String callbackId, JSONObject requestJsonObject
762     ) throws JSONException, WifiAwareManagerSnippetException {
763         checkWifiRttManager();
764         checkWifiRttAvailable();
765         RangingRequest request = WifiAwareJsonDeserializer.jsonToRangingRequest(
766                 requestJsonObject, mPeerHandles);
767         Log.v("Starting Wi-Fi RTT ranging with config: " + request.toString());
768         RangingCallback rangingCb = new RangingCallback(eventCache, callbackId);
769         mWifiRttManager.startRanging(request, command -> mHandler.post(command), rangingCb);
770     }
771 
772     /**
773      * Ranging result callback class.
774      */
775     private static class RangingCallback extends RangingResultCallback {
776         private static final String EVENT_NAME_RANGING_RESULT = "WifiRttRangingOnRangingResult";
777         private final EventCache mEventCache;
778         private final String mCallbackId;
779 
RangingCallback(EventCache eventCache, String callbackId)780         RangingCallback(EventCache eventCache, String callbackId) {
781             this.mEventCache = eventCache;
782             this.mCallbackId = callbackId;
783         }
784 
785         @Override
onRangingFailure(int code)786         public void onRangingFailure(int code) {
787             SnippetEvent event = new SnippetEvent(mCallbackId, EVENT_NAME_RANGING_RESULT);
788             event.getData().putString("callbackName", "onRangingFailure");
789             event.getData().putInt("statusCode", code);
790             mEventCache.postEvent(event);
791         }
792 
793         @Override
onRangingResults(List<RangingResult> results)794         public void onRangingResults(List<RangingResult> results) {
795             SnippetEvent event = new SnippetEvent(mCallbackId, EVENT_NAME_RANGING_RESULT);
796             event.getData().putString("callbackName", "onRangingResults");
797 
798             Bundle[] resultBundles = new Bundle[results.size()];
799             for (int i = 0; i < results.size(); i++) {
800                 RangingResult result = results.get(i);
801                 resultBundles[i] = new Bundle();
802                 resultBundles[i].putInt("status", result.getStatus());
803                 resultBundles[i].putInt("distanceMm", result.getDistanceMm());
804                 resultBundles[i].putInt("rssi", result.getRssi());
805                 PeerHandle peer = result.getPeerHandle();
806                 if (peer != null) {
807                     resultBundles[i].putInt("peerId", peer.hashCode());
808                 } else {
809                     resultBundles[i].putBundle("peerId", null);
810                 }
811                 MacAddress mac = result.getMacAddress();
812                 resultBundles[i].putString("mac", mac != null ? mac.toString() : null);
813             }
814             event.getData().putParcelableArray("results", resultBundles);
815             mEventCache.postEvent(event);
816         }
817     }
818 
819     /**
820      * Return whether this device supports setting a channel requirement in a data-path request.
821      */
822     @Rpc(
823             description = "Return whether this device supports setting a channel requirement in a "
824                 + "data-path request."
825     )
wifiAwareIsSetChannelOnDataPathSupported()826     public boolean wifiAwareIsSetChannelOnDataPathSupported() {
827         return mWifiAwareManager.isSetChannelOnDataPathSupported();
828     }
829 
830 }
831 
832