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.ranging; 18 19 import android.app.UiAutomation; 20 import android.content.Context; 21 import android.net.ConnectivityManager; 22 import android.net.wifi.WifiManager; 23 import android.ranging.RangingCapabilities; 24 import android.ranging.RangingData; 25 import android.ranging.RangingDevice; 26 import android.ranging.RangingManager; 27 import android.ranging.RangingManager.RangingTechnology; 28 import android.ranging.RangingMeasurement; 29 import android.ranging.RangingPreference; 30 import android.ranging.RangingSession; 31 import android.ranging.oob.TransportHandle; 32 import android.util.Log; 33 34 import androidx.annotation.NonNull; 35 import androidx.test.platform.app.InstrumentationRegistry; 36 37 import com.google.android.mobly.snippet.Snippet; 38 import com.google.android.mobly.snippet.event.EventCache; 39 import com.google.android.mobly.snippet.event.SnippetEvent; 40 import com.google.android.mobly.snippet.rpc.AsyncRpc; 41 import com.google.android.mobly.snippet.rpc.Rpc; 42 43 import org.json.JSONException; 44 import org.json.JSONObject; 45 46 import java.lang.reflect.Method; 47 import java.util.Map; 48 import java.util.UUID; 49 import java.util.concurrent.ConcurrentHashMap; 50 import java.util.concurrent.ConcurrentMap; 51 import java.util.concurrent.Executor; 52 import java.util.concurrent.Executors; 53 import java.util.concurrent.atomic.AtomicReference; 54 import java.util.function.Supplier; 55 56 public class RangingSnippet implements Snippet { 57 private static final String TAG = "GenericRangingSnippet"; 58 59 private final Context mContext; 60 private final RangingManager mRangingManager; 61 private final Executor mExecutor = Executors.newSingleThreadExecutor(); 62 private final EventCache mEventCache = EventCache.getInstance(); 63 private final ConnectivityManager mConnectivityManager; 64 private final WifiManager mWifiManager; 65 private final ConcurrentMap<String, RangingSessionInfo> mSessions; 66 private final ConcurrentMap<Integer, Integer> mTechnologyAvailability; 67 private final AtomicReference<RangingCapabilities> mRangingCapabilities = 68 new AtomicReference<>(); 69 RangingSnippet()70 public RangingSnippet() { 71 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 72 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); 73 mWifiManager = mContext.getSystemService(WifiManager.class); 74 mRangingManager = mContext.getSystemService(RangingManager.class); 75 76 mSessions = new ConcurrentHashMap<>(); 77 mTechnologyAvailability = new ConcurrentHashMap<>(); 78 mRangingManager.registerCapabilitiesCallback(mExecutor, new AvailabilityListener()); 79 } 80 adoptShellPermission()81 private void adoptShellPermission() { 82 UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 83 uia.adoptShellPermissionIdentity(); 84 try { 85 Class<?> cls = Class.forName("android.app.UiAutomation"); 86 Method destroyMethod = cls.getDeclaredMethod("destroy"); 87 destroyMethod.invoke(uia); 88 } catch (ReflectiveOperationException e) { 89 throw new IllegalStateException("Failed to cleanup ui automation", e); 90 } 91 } 92 dropShellPermission()93 private void dropShellPermission() throws Throwable { 94 UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 95 uia.dropShellPermissionIdentity(); 96 try { 97 Class<?> cls = Class.forName("android.app.UiAutomation"); 98 Method destroyMethod = cls.getDeclaredMethod("destroy"); 99 destroyMethod.invoke(uia); 100 } catch (ReflectiveOperationException e) { 101 throw new IllegalStateException("Failed to cleanup ui automation", e); 102 } 103 } 104 105 private enum Event { 106 OPENED, 107 OPEN_FAILED, 108 STARTED, 109 DATA, 110 STOPPED, 111 CLOSED, 112 OOB_SEND_CAPABILITIES_REQUEST, 113 OOB_SEND_CAPABILITIES_RESPONSE, 114 OOB_SEND_SET_CONFIGURATION, 115 OOB_SEND_STOP_RANGING, 116 OOB_SEND_UNKNOWN, 117 OOB_CLOSED 118 } 119 120 private class RangingSessionCallback implements RangingSession.Callback { 121 122 private final String mCallbackId; 123 RangingSessionCallback(String callbackId)124 RangingSessionCallback(String callbackId) { 125 mCallbackId = callbackId; 126 } 127 128 @Override onOpened()129 public void onOpened() { 130 Log.d(TAG, "onOpened"); 131 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.OPENED.toString())); 132 } 133 134 @Override onOpenFailed(@eason int reason)135 public void onOpenFailed(@Reason int reason) { 136 Log.d(TAG, "onOpenFailed"); 137 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.OPEN_FAILED.toString())); 138 } 139 140 @Override onStarted(@onNull RangingDevice peer, @RangingTechnology int technology)141 public void onStarted(@NonNull RangingDevice peer, @RangingTechnology int technology) { 142 Log.d(TAG, "onStarted"); 143 SnippetEvent event = new SnippetEvent(mCallbackId, Event.STARTED.toString()); 144 event.getData().putString("peer_id", peer.getUuid().toString()); 145 event.getData().putInt("technology", technology); 146 mEventCache.postEvent(event); 147 } 148 149 @Override onResults(@onNull RangingDevice peer, @NonNull RangingData data)150 public void onResults(@NonNull RangingDevice peer, @NonNull RangingData data) { 151 Log.d(TAG, "onData { peer: " + peer.getUuid() 152 + " Distance: " + data.getDistance() 153 + " Azimuth: " + data.getAzimuth() 154 + " Elevation: " + data.getElevation() 155 + " RangingTechnology: " + data.getRangingTechnology() 156 + " Timestamp: " + data.getTimestampMillis() 157 + " hasRssi: " + data.hasRssi() 158 + " getRssi: " + (data.hasRssi() ? data.getRssi() : "null") 159 + " }"); 160 RangingMeasurement distance = data.getDistance(); 161 if (distance != null) { 162 Log.d(TAG, " Distance: " + distance.getMeasurement() 163 + " Confidence: " + distance.getConfidence()); 164 } 165 SnippetEvent event = new SnippetEvent(mCallbackId, Event.DATA.toString()); 166 event.getData().putString("peer_id", peer.getUuid().toString()); 167 event.getData().putInt("technology", data.getRangingTechnology()); 168 mEventCache.postEvent(event); 169 } 170 171 @Override onStopped(@onNull RangingDevice peer, @RangingTechnology int technology)172 public void onStopped(@NonNull RangingDevice peer, @RangingTechnology int technology) { 173 Log.d(TAG, "onStopped"); 174 SnippetEvent event = new SnippetEvent(mCallbackId, Event.STOPPED.toString()); 175 event.getData().putString("peer_id", peer.getUuid().toString()); 176 event.getData().putInt("technology", technology); 177 mEventCache.postEvent(event); 178 } 179 180 @Override onClosed(@eason int reason)181 public void onClosed(@Reason int reason) { 182 Log.d(TAG, "onClosed"); 183 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.CLOSED.toString())); 184 } 185 } 186 187 static class RangingSessionInfo { 188 private final RangingSession mSession; 189 private final RangingSessionCallback mCallback; 190 private final ConcurrentMap<RangingDevice, OobTransportImpl> mOobTransports; 191 RangingSessionInfo(RangingSession session, RangingSessionCallback callback)192 RangingSessionInfo(RangingSession session, RangingSessionCallback callback) { 193 mSession = session; 194 mCallback = callback; 195 mOobTransports = new ConcurrentHashMap<>(); 196 } 197 getSession()198 public RangingSession getSession() { 199 return mSession; 200 } 201 getCallback()202 public RangingSessionCallback getCallback() { 203 return mCallback; 204 } 205 206 } 207 208 private class AvailabilityListener implements RangingManager.RangingCapabilitiesCallback { 209 @Override onRangingCapabilities(@onNull RangingCapabilities capabilities)210 public void onRangingCapabilities(@NonNull RangingCapabilities capabilities) { 211 Log.d(TAG, " Ranging capabilities " + capabilities); 212 Map<Integer, Integer> availabilities = capabilities.getTechnologyAvailability(); 213 mTechnologyAvailability.putAll(availabilities); 214 mRangingCapabilities.set(capabilities); 215 } 216 } 217 218 219 class OobTransportFactory { 220 private final String mCallbackId; 221 private final RangingSessionInfo mSessionInfo; 222 OobTransportFactory(String callbackId, RangingSessionInfo sessionInfo)223 OobTransportFactory(String callbackId, RangingSessionInfo sessionInfo) { 224 mCallbackId = callbackId; 225 mSessionInfo = sessionInfo; 226 } 227 createOobTransport(RangingDevice peer)228 public OobTransportImpl createOobTransport(RangingDevice peer) { 229 OobTransportImpl transport = new OobTransportImpl(mCallbackId, peer); 230 mSessionInfo.mOobTransports.put(peer, transport); 231 return transport; 232 } 233 } 234 235 class OobTransportImpl implements TransportHandle { 236 private final String mCallbackId; 237 private final RangingDevice mPeer; 238 private ReceiveCallback mReceiveCallback; 239 OobTransportImpl(String callbackId, RangingDevice peer)240 OobTransportImpl(String callbackId, RangingDevice peer) { 241 mCallbackId = callbackId; 242 mPeer = peer; 243 } 244 getOobEvent(@onNull byte[] data)245 private SnippetEvent getOobEvent(@NonNull byte[] data) { 246 int messageType = data[1]; 247 switch (messageType) { 248 case 0: 249 return new SnippetEvent( 250 mCallbackId, 251 Event.OOB_SEND_CAPABILITIES_REQUEST.toString()); 252 case 1: 253 return new SnippetEvent( 254 mCallbackId, 255 Event.OOB_SEND_CAPABILITIES_RESPONSE.toString()); 256 case 2: 257 return new SnippetEvent( 258 mCallbackId, 259 Event.OOB_SEND_SET_CONFIGURATION.toString()); 260 case 6: 261 return new SnippetEvent( 262 mCallbackId, 263 Event.OOB_SEND_STOP_RANGING.toString()); 264 default: 265 return new SnippetEvent( 266 mCallbackId, 267 Event.OOB_SEND_UNKNOWN.toString()); 268 } 269 } 270 @Override sendData(@onNull byte[] data)271 public void sendData(@NonNull byte[] data) { 272 SnippetEvent event = getOobEvent(data); 273 event.getData().putString("peer_id", mPeer.getUuid().toString()); 274 event.getData().putByteArray("data", data); 275 mEventCache.postEvent(event); 276 } 277 278 @Override registerReceiveCallback( @onNull Executor executor, @NonNull ReceiveCallback callback )279 public void registerReceiveCallback( 280 @NonNull Executor executor, @NonNull ReceiveCallback callback 281 ) { 282 mReceiveCallback = callback; 283 } 284 285 @Override close()286 public void close() throws Exception { 287 Log.d(TAG, "TransportHandle close"); 288 SnippetEvent event = new SnippetEvent(mCallbackId, Event.OOB_CLOSED.toString()); 289 event.getData().putString("peer_id", mPeer.getUuid().toString()); 290 mEventCache.postEvent(event); 291 } 292 } 293 294 @AsyncRpc(description = "Start a ranging session") startRanging( String callbackId, String sessionHandle, JSONObject j )295 public void startRanging( 296 String callbackId, String sessionHandle, JSONObject j 297 ) throws JSONException { 298 299 RangingSessionCallback callback = new RangingSessionCallback(callbackId); 300 RangingSession session = mRangingManager.createRangingSession(mExecutor, callback); 301 RangingSessionInfo sessionInfo = new RangingSessionInfo(session, callback); 302 mSessions.put(sessionHandle, sessionInfo); 303 304 RangingPreference preference = 305 new RangingPreferenceConverter(new OobTransportFactory(callbackId, sessionInfo)) 306 .deserialize(j, RangingPreference.class); 307 308 session.start(preference); 309 } 310 311 @AsyncRpc(description = "Stop a ranging session") stopRanging(String unused, String sessionHandle)312 public void stopRanging(String unused, String sessionHandle) { 313 RangingSessionInfo sessionInfo = mSessions.get(sessionHandle); 314 if (sessionInfo != null) { 315 sessionInfo.getSession().stop(); 316 mSessions.remove(sessionHandle); 317 } 318 } 319 320 @Rpc(description = "Handle data received from a peer via OOB") handleOobDataReceived(String sessionHandle, String peerId, byte[] data)321 public void handleOobDataReceived(String sessionHandle, String peerId, byte[] data) { 322 mSessions.get(sessionHandle) 323 .mOobTransports.get(new RangingDevice.Builder() 324 .setUuid(UUID.fromString(peerId)) 325 .build()) 326 .mReceiveCallback.onReceiveData(data); 327 } 328 329 @Rpc(description = "Handle an OOB peer disconnecting") handleOobPeerDisconnected(String sessionHandle, String peerId)330 public void handleOobPeerDisconnected(String sessionHandle, String peerId) { 331 mSessions.get(sessionHandle) 332 .mOobTransports.get(new RangingDevice.Builder() 333 .setUuid(UUID.fromString(peerId)) 334 .build()) 335 .mReceiveCallback.onDisconnect(); 336 } 337 338 @Rpc(description = "Handle an OOB peer reconnecting") handleOobPeerReconnect(String sessionHandle, String peerId)339 public void handleOobPeerReconnect(String sessionHandle, String peerId) { 340 mSessions.get(sessionHandle) 341 .mOobTransports.get(new RangingDevice.Builder() 342 .setUuid(UUID.fromString(peerId)) 343 .build()) 344 .mReceiveCallback.onReconnect(); 345 } 346 347 @Rpc(description = "Handle an OOB transport closing") handleOobClosed(String sessionHandle, String peerId)348 public void handleOobClosed(String sessionHandle, String peerId) { 349 mSessions.get(sessionHandle) 350 .mOobTransports.get(new RangingDevice.Builder() 351 .setUuid(UUID.fromString(peerId)) 352 .build()) 353 .mReceiveCallback.onClose(); 354 } 355 356 @Rpc(description = "Check whether the provided ranging technology is enabled") isTechnologyEnabled(int technology)357 public boolean isTechnologyEnabled(int technology) { 358 Integer availability = mTechnologyAvailability.get(technology); 359 return availability != null 360 && availability == RangingCapabilities.ENABLED; 361 } 362 363 @Rpc(description = "Check whether the provided ranging technology is supported") isTechnologySupported(int technology)364 public boolean isTechnologySupported(int technology) { 365 Integer availability = mTechnologyAvailability.get(technology); 366 return availability != null 367 && availability != RangingCapabilities.NOT_SUPPORTED; 368 } 369 370 @Rpc(description = "Check whether periodic RTT ranging technology is supported") hasPeriodicRangingHwFeature()371 public boolean hasPeriodicRangingHwFeature() { 372 RangingCapabilities capabilities = mRangingCapabilities.get(); 373 if (capabilities == null) { 374 return false; 375 } 376 return capabilities.getRttRangingCapabilities().hasPeriodicRangingHardwareFeature(); 377 } 378 379 @Rpc(description = "Set airplane mode") setAirplaneMode(boolean enabled)380 public void setAirplaneMode(boolean enabled) throws Throwable { 381 runWithShellPermission(() -> mConnectivityManager.setAirplaneMode(enabled)); 382 } 383 384 @Rpc(description = "Set wifi mode") setWifiEnabled(boolean enabled)385 public void setWifiEnabled(boolean enabled) throws Throwable { 386 runWithShellPermission(() -> mWifiManager.setWifiEnabled(enabled)); 387 } 388 389 @Rpc(description = "Return wifi mode") isWifiEnabled()390 public boolean isWifiEnabled() throws Throwable { 391 return runWithShellPermission(() -> mWifiManager.isWifiEnabled()); 392 } 393 394 @Rpc(description = "Log info level message to device logcat") logInfo(String message)395 public void logInfo(String message) { 396 Log.i(TAG, message); 397 } 398 runWithShellPermission(Runnable action)399 public void runWithShellPermission(Runnable action) throws Throwable { 400 adoptShellPermission(); 401 try { 402 action.run(); 403 } finally { 404 dropShellPermission(); 405 } 406 } 407 runWithShellPermission(ThrowingSupplier<T> action)408 public <T> T runWithShellPermission(ThrowingSupplier<T> action) throws Throwable { 409 adoptShellPermission(); 410 try { 411 return action.get(); 412 } finally { 413 dropShellPermission(); 414 } 415 } 416 417 /** 418 * Similar to {@link Supplier} but has {@code throws Exception}. 419 * 420 * @param <T> type of the value produced 421 */ 422 public interface ThrowingSupplier<T> { 423 /** 424 * Similar to {@link Supplier#get} but has {@code throws Exception}. 425 */ get()426 T get() throws Exception; 427 } 428 } 429