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