• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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