1 /*
2  * Copyright 2022 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.uwb;
18 
19 import android.app.UiAutomation;
20 import android.content.Context;
21 import android.net.ConnectivityManager;
22 import android.os.Build;
23 import android.os.PersistableBundle;
24 import android.uwb.RangingMeasurement;
25 import android.uwb.RangingReport;
26 import android.uwb.RangingSession;
27 import android.uwb.UwbAddress;
28 import android.uwb.UwbManager;
29 
30 import androidx.test.platform.app.InstrumentationRegistry;
31 
32 import com.google.android.mobly.snippet.Snippet;
33 import com.google.android.mobly.snippet.event.EventCache;
34 import com.google.android.mobly.snippet.event.SnippetEvent;
35 import com.google.android.mobly.snippet.rpc.AsyncRpc;
36 import com.google.android.mobly.snippet.rpc.Rpc;
37 import com.google.android.mobly.snippet.util.Log;
38 import com.google.uwb.support.ccc.CccOpenRangingParams;
39 import com.google.uwb.support.ccc.CccParams;
40 import com.google.uwb.support.ccc.CccPulseShapeCombo;
41 import com.google.uwb.support.ccc.CccRangingStartedParams;
42 import com.google.uwb.support.fira.FiraControleeParams;
43 import com.google.uwb.support.fira.FiraOpenSessionParams;
44 import com.google.uwb.support.fira.FiraParams;
45 import com.google.uwb.support.fira.FiraRangingReconfigureParams;
46 
47 import org.json.JSONArray;
48 import org.json.JSONException;
49 import org.json.JSONObject;
50 
51 import java.lang.reflect.Method;
52 import java.util.Arrays;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Set;
56 import java.util.concurrent.Executor;
57 import java.util.concurrent.Executors;
58 import java.util.function.Supplier;
59 
60 /** Snippet class exposing Android APIs for Uwb. */
61 public class UwbManagerSnippet implements Snippet {
62     private static class UwbManagerSnippetException extends Exception {
63 
UwbManagerSnippetException(String msg)64         UwbManagerSnippetException(String msg) {
65             super(msg);
66         }
67 
UwbManagerSnippetException(String msg, Throwable err)68         UwbManagerSnippetException(String msg, Throwable err) {
69             super(msg, err);
70         }
71     }
72 
73     private static final String TAG = "UwbManagerSnippet: ";
74     private final UwbManager mUwbManager;
75     private final ConnectivityManager mConnectivityManager;
76     private final Context mContext;
77     private final Executor mExecutor = Executors.newSingleThreadExecutor();
78     private final EventCache mEventCache = EventCache.getInstance();
79     private static HashMap<String, RangingSessionCallback> sRangingSessionCallbackMap =
80             new HashMap<String, RangingSessionCallback>();
81     private static HashMap<String, UwbAdapterStateCallback> sUwbAdapterStateCallbackMap =
82             new HashMap<String, UwbAdapterStateCallback>();
83 
UwbManagerSnippet()84     public UwbManagerSnippet() {
85         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
86         mUwbManager = mContext.getSystemService(UwbManager.class);
87         mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
88     }
89 
90     private enum Event {
91         Invalid(0),
92         Opened(1 << 0),
93         Started(1 << 1),
94         Reconfigured(1 << 2),
95         Stopped(1 << 3),
96         Closed(1 << 4),
97         OpenFailed(1 << 5),
98         StartFailed(1 << 6),
99         ReconfigureFailed(1 << 7),
100         StopFailed(1 << 8),
101         CloseFailed(1 << 9),
102         ReportReceived(1 << 10),
103         ControleeAdded(1 << 11),
104         ControleeAddFailed(1 << 12),
105         ControleeRemoved(1 << 13),
106         ControleeRemoveFailed(1 << 14),
107         Paused(1 << 15),
108         PauseFailed(1 << 16),
109         Resumed(1 << 17),
110         ResumeFailed(1 << 18),
111         DataSent(1 << 19),
112         DataSendFailed(1 << 20),
113         DataReceived(1 << 21),
114         DataReceiveFailed(1 << 22),
115         ServiceDiscovered(1 << 23),
116         ServiceConnected(1 << 24),
117         RangingRoundsUpdateDtTagStatus(1 << 25),
118         EventAll(
119                 1 << 0
120                 | 1 << 1
121                 | 1 << 2
122                 | 1 << 3
123                 | 1 << 4
124                 | 1 << 5
125                 | 1 << 6
126                 | 1 << 7
127                 | 1 << 8
128                 | 1 << 9
129                 | 1 << 10
130                 | 1 << 11
131                 | 1 << 12
132                 | 1 << 13
133                 | 1 << 14
134                 | 1 << 15
135                 | 1 << 16
136                 | 1 << 17
137                 | 1 << 18
138                 | 1 << 19
139                 | 1 << 20
140                 | 1 << 21
141                 | 1 << 22
142                 | 1 << 23
143                 | 1 << 24
144                 | 1 << 25
145         );
146 
147         private final int mType;
Event(int type)148         Event(int type) {
149             mType = type;
150         }
getType()151         private int getType() {
152             return mType;
153         }
154     }
155 
156     class UwbAdapterStateCallback implements UwbManager.AdapterStateCallback {
157 
158         public String mId;
159 
UwbAdapterStateCallback(String id)160         UwbAdapterStateCallback(String id) {
161             mId = id;
162         }
163 
toString(int state)164         public String toString(int state) {
165             switch (state) {
166                 case 1: return "Inactive";
167                 case 2: return "Active";
168                 case 3: return "HwIdle";
169                 default: return "Disabled";
170             }
171         }
172 
173         @Override
onStateChanged(int state, int reason)174         public void onStateChanged(int state, int reason) {
175             Log.d(TAG + "UwbAdapterStateCallback#onStateChanged() called");
176             Log.d(TAG + "Adapter state " + String.valueOf(state)
177                     + ", state changed reason " + String.valueOf(reason));
178             SnippetEvent event = new SnippetEvent(mId, "UwbAdapterStateCallback");
179             event.getData().putString("uwbAdapterStateEvent", toString(state));
180             mEventCache.postEvent(event);
181         }
182     }
183 
184     class RangingSessionCallback implements RangingSession.Callback {
185 
186         public RangingSession rangingSession;
187         public PersistableBundle persistableBundle;
188         public PersistableBundle sessionInfo;
189         public RangingReport rangingReport;
190         public String mId;
191         public UwbAddress uwbAddress;
192         public byte[] dataReceived;
193 
RangingSessionCallback(String id, int events)194         RangingSessionCallback(String id, int events) {
195             mId = id;
196         }
197 
handleEvent(Event e)198         private void handleEvent(Event e) {
199             Log.d(TAG + "RangingSessionCallback#handleEvent() for " + e.toString());
200             SnippetEvent event = new SnippetEvent(mId, "RangingSessionCallback");
201             event.getData().putString("rangingSessionEvent", e.toString());
202             mEventCache.postEvent(event);
203         }
204 
handleEvent(Event e, int reason)205         private void handleEvent(Event e, int reason) {
206             Log.d(TAG + "RangingSessionCallback#handleEvent() for " + e.toString());
207             SnippetEvent event = new SnippetEvent(mId, "RangingSessionCallback");
208             event.getData().putString("rangingSessionEvent", e.toString());
209             event.getData().putInt("reasonCode", reason);
210             mEventCache.postEvent(event);
211         }
212 
213         @Override
onOpened(RangingSession session)214         public void onOpened(RangingSession session) {
215             Log.d(TAG + "RangingSessionCallback#onOpened() called");
216             rangingSession = session;
217             handleEvent(Event.Opened);
218         }
219 
220         @Override
onOpenFailed(int reason, PersistableBundle params)221         public void onOpenFailed(int reason, PersistableBundle params) {
222             Log.d(TAG + "RangingSessionCallback#onOpenedFailed() called");
223             Log.d(TAG + "OpenFailed reason " + String.valueOf(reason));
224             persistableBundle = params;
225             handleEvent(Event.OpenFailed, reason);
226         }
227 
228         @Override
onStarted(PersistableBundle info)229         public void onStarted(PersistableBundle info) {
230             Log.d(TAG + "RangingSessionCallback#onStarted() called");
231             sessionInfo = info;
232             handleEvent(Event.Started);
233         }
234 
235         @Override
onStartFailed(int reason, PersistableBundle params)236         public void onStartFailed(int reason, PersistableBundle params) {
237             Log.d(TAG + "RangingSessionCallback#onStartFailed() called");
238             Log.d(TAG + "StartFailed reason " + String.valueOf(reason));
239             persistableBundle = params;
240             handleEvent(Event.StartFailed, reason);
241         }
242 
243         @Override
onReconfigured(PersistableBundle params)244         public void onReconfigured(PersistableBundle params) {
245             Log.d(TAG + "RangingSessionCallback#oniReconfigured() called");
246             persistableBundle = params;
247             handleEvent(Event.Reconfigured);
248         }
249 
250         @Override
onReconfigureFailed(int reason, PersistableBundle params)251         public void onReconfigureFailed(int reason, PersistableBundle params) {
252             Log.d(TAG + "RangingSessionCallback#onReconfigureFailed() called");
253             Log.d(TAG + "ReconfigureFailed reason " + String.valueOf(reason));
254             persistableBundle = params;
255             handleEvent(Event.ReconfigureFailed, reason);
256         }
257 
258         @Override
onStopped(int reason, PersistableBundle params)259         public void onStopped(int reason, PersistableBundle params) {
260             Log.d(TAG + "RangingSessionCallback#onStopped() called");
261             Log.d(TAG + "Stopped reason " + String.valueOf(reason));
262             persistableBundle = params;
263             handleEvent(Event.Stopped, reason);
264         }
265 
266         @Override
onStopFailed(int reason, PersistableBundle params)267         public void onStopFailed(int reason, PersistableBundle params) {
268             Log.d(TAG + "RangingSessionCallback#onStopFailed() called");
269             Log.d(TAG + "StopFailed reason " + String.valueOf(reason));
270             persistableBundle = params;
271             handleEvent(Event.StopFailed, reason);
272         }
273 
274         @Override
onClosed(int reason, PersistableBundle params)275         public void onClosed(int reason, PersistableBundle params) {
276             Log.d(TAG + "RangingSessionCallback#onClosed() called");
277             Log.d(TAG + "Closed reason " + String.valueOf(reason));
278             persistableBundle = params;
279             handleEvent(Event.Closed, reason);
280         }
281 
282         @Override
onReportReceived(RangingReport report)283         public void onReportReceived(RangingReport report) {
284             Log.d(TAG + "RangingSessionCallback#onReportReceived() called");
285             rangingReport = report;
286             handleEvent(Event.ReportReceived);
287         }
288 
289         @Override
onControleeAdded(PersistableBundle params)290         public void onControleeAdded(PersistableBundle params) {
291             Log.d(TAG + "RangingSessionCallback#onControleeAdded() called");
292             persistableBundle = params;
293             handleEvent(Event.ControleeAdded);
294 
295         }
296 
297         @Override
onControleeAddFailed( int reason, PersistableBundle params)298         public void onControleeAddFailed(
299                 int reason, PersistableBundle params) {
300             Log.d(TAG + "RangingSessionCallback#onControleeAddFailed() called");
301             Log.d(TAG + "ControleeAddFailed reason " + String.valueOf(reason));
302             persistableBundle = params;
303             handleEvent(Event.ControleeAddFailed, reason);
304 
305         }
306 
307         @Override
onControleeRemoved(PersistableBundle params)308         public void onControleeRemoved(PersistableBundle params) {
309             Log.d(TAG + "RangingSessionCallback#onControleeRemoved() called");
310             persistableBundle = params;
311             handleEvent(Event.ControleeRemoved);
312         }
313 
314         @Override
onControleeRemoveFailed( int reason, PersistableBundle params)315         public void onControleeRemoveFailed(
316                 int reason, PersistableBundle params) {
317             Log.d(TAG + "RangingSessionCallback#onControleeRemoveFailed() called");
318             Log.d(TAG + "ControleeRemoveFailed reason " + String.valueOf(reason));
319             persistableBundle = params;
320             handleEvent(Event.ControleeRemoveFailed, reason);
321         }
322 
323         @Override
onPaused(PersistableBundle params)324         public void onPaused(PersistableBundle params) {
325             Log.d(TAG + "RangingSessionCallback#onPaused() called");
326             persistableBundle = params;
327             handleEvent(Event.Paused);
328         }
329 
330         @Override
onPauseFailed(int reason, PersistableBundle params)331         public void onPauseFailed(int reason, PersistableBundle params) {
332             Log.d(TAG + "RangingSessionCallback#onPauseFailed() called");
333             Log.d(TAG + "PauseFailed reason " + String.valueOf(reason));
334             persistableBundle = params;
335             handleEvent(Event.PauseFailed, reason);
336         }
337 
338         @Override
onResumed(PersistableBundle params)339         public void onResumed(PersistableBundle params) {
340             Log.d(TAG + "RangingSessionCallback#onResumed() called");
341             persistableBundle = params;
342             handleEvent(Event.Resumed);
343         }
344 
345         @Override
onResumeFailed(int reason, PersistableBundle params)346         public void onResumeFailed(int reason, PersistableBundle params) {
347             Log.d(TAG + "RangingSessionCallback#onResumeFailed() called");
348             Log.d(TAG + "ResumeFailed reason " + String.valueOf(reason));
349             persistableBundle = params;
350             handleEvent(Event.ResumeFailed, reason);
351         }
352 
353         @Override
onDataSent(UwbAddress remoteDeviceAddress, PersistableBundle params)354         public void onDataSent(UwbAddress remoteDeviceAddress,
355                 PersistableBundle params) {
356             Log.d(TAG + "RangingSessionCallback#onDataSent() called");
357             uwbAddress = getComputedMacAddress(remoteDeviceAddress);
358             persistableBundle = params;
359             handleEvent(Event.DataSent);
360         }
361 
362         @Override
onDataSendFailed(UwbAddress remoteDeviceAddress, int reason, PersistableBundle params)363         public void onDataSendFailed(UwbAddress remoteDeviceAddress,
364                 int reason, PersistableBundle params) {
365             Log.d(TAG + "RangingSessionCallback#onDataSendFailed() called");
366             Log.d(TAG + "DataSendFailed reason " + String.valueOf(reason));
367             uwbAddress = getComputedMacAddress(remoteDeviceAddress);
368             persistableBundle = params;
369             handleEvent(Event.DataSendFailed, reason);
370         }
371 
372         @Override
onDataReceived(UwbAddress remoteDeviceAddress, PersistableBundle params, byte[] data)373         public void onDataReceived(UwbAddress remoteDeviceAddress,
374                 PersistableBundle params, byte[] data) {
375             Log.d(TAG + "RangingSessionCallback#onDataReceived() called");
376             uwbAddress = getComputedMacAddress(remoteDeviceAddress);
377             dataReceived = data;
378             persistableBundle = params;
379             handleEvent(Event.DataReceived);
380         }
381 
382         @Override
onDataReceiveFailed(UwbAddress remoteDeviceAddress, int reason, PersistableBundle params)383         public void onDataReceiveFailed(UwbAddress remoteDeviceAddress,
384                 int reason, PersistableBundle params) {
385             Log.d(TAG + "RangingSessionCallback#onDataReceiveFailed() called");
386             Log.d(TAG + "DataReceiveFailed reason " + String.valueOf(reason));
387             uwbAddress = getComputedMacAddress(remoteDeviceAddress);
388             persistableBundle = params;
389             handleEvent(Event.DataReceiveFailed, reason);
390         }
391 
392         @Override
onServiceDiscovered(PersistableBundle params)393         public void onServiceDiscovered(PersistableBundle params) {
394             Log.d(TAG + "RangingSessionCallback#onServiceDiscovered() called");
395             persistableBundle = params;
396             handleEvent(Event.ServiceDiscovered);
397         }
398 
399         @Override
onServiceConnected(PersistableBundle params)400         public void onServiceConnected(PersistableBundle params) {
401             Log.d(TAG + "RangingSessionCallback#onServiceConnected() called");
402             persistableBundle = params;
403             handleEvent(Event.ServiceConnected);
404         }
405 
406         // TODO: This is only available in Android U SDK. So, expose it there only.
onRangingRoundsUpdateDtTagStatus(PersistableBundle params)407         public void onRangingRoundsUpdateDtTagStatus(PersistableBundle params) {
408             Log.d(TAG + "RangingSessionCallback#onRangingRoundsUpdateDtTagStatus() called");
409             persistableBundle = params;
410             handleEvent(Event.RangingRoundsUpdateDtTagStatus);
411         }
412     }
413 
414     /** Register uwb adapter state callback. */
415     @AsyncRpc(description = "Register uwb adapter state callback")
registerUwbAdapterStateCallback(String callbackId, String key)416     public void registerUwbAdapterStateCallback(String callbackId, String key) throws Throwable {
417         UwbAdapterStateCallback uwbAdapterStateCallback = new UwbAdapterStateCallback(callbackId);
418         sUwbAdapterStateCallbackMap.put(key, uwbAdapterStateCallback);
419         runWithShellPermission(() ->
420                 mUwbManager.registerAdapterStateCallback(mExecutor, uwbAdapterStateCallback));
421     }
422 
423     /** Unregister uwb adapter state callback. */
424     @Rpc(description = "Unregister uwb adapter state callback.")
unregisterUwbAdapterStateCallback(String key)425     public void unregisterUwbAdapterStateCallback(String key) throws Throwable {
426         UwbAdapterStateCallback uwbAdapterStateCallback = sUwbAdapterStateCallbackMap.get(key);
427         runWithShellPermission(() ->
428                 mUwbManager.unregisterAdapterStateCallback(uwbAdapterStateCallback));
429         sUwbAdapterStateCallbackMap.remove(key);
430     }
431 
432     /** Get UWB adapter state. */
433     @Rpc(description = "Get Uwb adapter state")
getAdapterState()434     public int getAdapterState() throws Throwable {
435         return runWithShellPermission(() -> mUwbManager.getAdapterState());
436     }
437 
438     /** Get the UWB state. */
439     @Rpc(description = "Get Uwb state")
isUwbEnabled()440     public boolean isUwbEnabled() throws Throwable {
441         return runWithShellPermission(() -> mUwbManager.isUwbEnabled());
442     }
443 
444     /** Set the UWB state. */
445     @Rpc(description = "Set Uwb state")
setUwbEnabled(boolean enabled)446     public void setUwbEnabled(boolean enabled) throws Throwable {
447         runWithShellPermission(() -> mUwbManager.setUwbEnabled(enabled));
448     }
449 
450     /** Get the UWB hardware state. */
451     @Rpc(description = "Get Uwb hardware state")
isUwbHwEnableRequested()452     public boolean isUwbHwEnableRequested() throws Throwable {
453         return runWithShellPermission(() -> mUwbManager.isUwbHwEnableRequested());
454     }
455 
456     /** Set the UWB hardware state. */
457     @Rpc(description = "Set Uwb hardware state")
requestUwbHwEnabled(boolean enabled)458     public void requestUwbHwEnabled(boolean enabled) throws Throwable {
459         runWithShellPermission(() -> mUwbManager.requestUwbHwEnabled(enabled));
460     }
461 
462     /** Get UWB HW idle feature state. */
463     @Rpc(description = "Get Uwb hardware idle feature state")
isUwbHwIdleTurnOffEnabled()464     public boolean isUwbHwIdleTurnOffEnabled() throws Throwable {
465         return runWithShellPermission(() -> mUwbManager.isUwbHwIdleTurnOffEnabled());
466     }
467 
convertJSONArrayToByteArray(JSONArray jArray)468     private byte[] convertJSONArrayToByteArray(JSONArray jArray) throws JSONException {
469         if (jArray == null) {
470             return null;
471         }
472         byte[] bArray = new byte[jArray.length()];
473         for (int i = 0; i < jArray.length(); i++) {
474             bArray[i] = (byte) jArray.getInt(i);
475         }
476         return bArray;
477     }
478 
generateFiraRangingReconfigureParams(JSONObject j)479     private FiraRangingReconfigureParams generateFiraRangingReconfigureParams(JSONObject j)
480             throws JSONException {
481         if (j == null) {
482             return null;
483         }
484         FiraRangingReconfigureParams.Builder builder = new FiraRangingReconfigureParams.Builder();
485         if (j.has("action")) {
486             builder.setAction(j.getInt("action"));
487         }
488         if (j.has("addressList")) {
489             JSONArray jArray = j.getJSONArray("addressList");
490             UwbAddress[] addressList = new UwbAddress[jArray.length()];
491             for (int i = 0; i < jArray.length(); i++) {
492                 addressList[i] = getComputedMacAddress(UwbAddress.fromBytes(
493                         convertJSONArrayToByteArray(jArray.getJSONArray(i))));
494             }
495             builder.setAddressList(addressList);
496         }
497         if (j.has("subSessionIdList")) {
498             JSONArray jArray = j.getJSONArray("subSessionIdList");
499             int[] subSessionIdList = new int[jArray.length()];
500             for (int i = 0; i < jArray.length(); i++) {
501                 subSessionIdList[i] = jArray.getInt(i);
502             }
503             builder.setSubSessionIdList(subSessionIdList);
504         }
505         if (j.has("subSessionKeyList")) {
506             JSONArray jSubSessionKeyListArray = j.getJSONArray("subSessionKeyList");
507             builder.setSubSessionKeyList(convertJSONArrayToByteArray(jSubSessionKeyListArray));
508         }
509         if (j.has("blockStrideLength")) {
510             builder.setBlockStrideLength(j.getInt("blockStrideLength"));
511         }
512         return builder.build();
513     }
514 
generateFiraControleeParams(JSONObject j)515     private FiraControleeParams generateFiraControleeParams(JSONObject j) throws JSONException {
516         if (j == null) {
517             return null;
518         }
519         FiraControleeParams.Builder builder = new FiraControleeParams.Builder();
520         if (j.has("action")) {
521             builder.setAction(j.getInt("action"));
522         }
523         if (j.has("addressList")) {
524             JSONArray jArray = j.getJSONArray("addressList");
525             UwbAddress[] addressList = new UwbAddress[jArray.length()];
526             for (int i = 0; i < jArray.length(); i++) {
527                 addressList[i] = getComputedMacAddress(UwbAddress.fromBytes(
528                         convertJSONArrayToByteArray(jArray.getJSONArray(i))));
529             }
530             builder.setAddressList(addressList);
531         }
532         if (j.has("subSessionIdList")) {
533             JSONArray jArray = j.getJSONArray("subSessionIdList");
534             int[] subSessionIdList = new int[jArray.length()];
535             for (int i = 0; i < jArray.length(); i++) {
536                 subSessionIdList[i] = jArray.getInt(i);
537             }
538             builder.setSubSessionIdList(subSessionIdList);
539         }
540         if (j.has("subSessionKeyList")) {
541             JSONArray jSubSessionKeyListArray = j.getJSONArray("subSessionKeyList");
542             builder.setSubSessionKeyList(convertJSONArrayToByteArray(jSubSessionKeyListArray));
543         }
544         return builder.build();
545     }
546 
generateCccRangingStartedParams(JSONObject j)547     private CccRangingStartedParams generateCccRangingStartedParams(JSONObject j)
548             throws JSONException {
549         if (j == null) {
550             return null;
551         }
552         CccRangingStartedParams.Builder builder = new CccRangingStartedParams.Builder();
553         if (j.has("stsIndex")) {
554             builder.setStartingStsIndex(j.getInt("stsIndex"));
555         }
556         if (j.has("uwbTime")) {
557             builder.setUwbTime0(j.getInt("uwbTime"));
558         }
559         if (j.has("hopModeKey")) {
560             builder.setHopModeKey(j.getInt("hopModeKey"));
561         }
562         if (j.has("syncCodeIndex")) {
563             builder.setSyncCodeIndex(j.getInt("syncCodeIndex"));
564         }
565         if (j.has("ranMultiplier")) {
566             builder.setRanMultiplier(j.getInt("ranMultiplier"));
567         }
568 
569         return builder.build();
570     }
571 
generateCccOpenRangingParams(JSONObject j)572     private CccOpenRangingParams generateCccOpenRangingParams(JSONObject j) throws JSONException {
573         if (j == null) {
574             return null;
575         }
576         CccOpenRangingParams.Builder builder = new CccOpenRangingParams.Builder();
577         builder.setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0);
578         if (j.has("sessionId")) {
579             builder.setSessionId(j.getInt("sessionId"));
580         }
581         if (j.has("uwbConfig")) {
582             builder.setUwbConfig(j.getInt("uwbConfig"));
583         }
584         if (j.has("ranMultiplier")) {
585             builder.setRanMultiplier(j.getInt("ranMultiplier"));
586         }
587         if (j.has("channel")) {
588             builder.setChannel(j.getInt("channel"));
589         }
590         if (j.has("chapsPerSlot")) {
591             builder.setNumChapsPerSlot(j.getInt("chapsPerSlot"));
592         }
593         if (j.has("responderNodes")) {
594             builder.setNumResponderNodes(j.getInt("responderNodes"));
595         }
596         if (j.has("slotsPerRound")) {
597             builder.setNumSlotsPerRound(j.getInt("slotsPerRound"));
598         }
599         if (j.has("hoppingMode")) {
600             builder.setHoppingConfigMode(j.getInt("hoppingMode"));
601         }
602         if (j.has("hoppingSequence")) {
603             builder.setHoppingSequence(j.getInt("hoppingSequence"));
604         }
605         if (j.has("syncCodeIndex")) {
606             builder.setSyncCodeIndex(j.getInt("syncCodeIndex"));
607         }
608         if (j.has("pulseShapeCombo")) {
609             JSONObject pulseShapeCombo = j.getJSONObject("pulseShapeCombo");
610             builder.setPulseShapeCombo(new CccPulseShapeCombo(
611                     pulseShapeCombo.getInt("pulseShapeComboTx"),
612                     pulseShapeCombo.getInt("pulseShapeComboRx")));
613         }
614 
615         return builder.build();
616     }
617 
generateFiraOpenSessionParams(JSONObject j)618     private FiraOpenSessionParams generateFiraOpenSessionParams(JSONObject j) throws JSONException {
619         if (j == null) {
620             return null;
621         }
622         FiraOpenSessionParams.Builder builder = new FiraOpenSessionParams.Builder();
623         builder.setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1);
624         if (j.has("sessionId")) {
625             builder.setSessionId(j.getInt("sessionId"));
626         }
627         if (j.has("deviceType")) {
628             builder.setDeviceType(j.getInt("deviceType"));
629         }
630         if (j.has("deviceRole")) {
631             builder.setDeviceRole(j.getInt("deviceRole"));
632         }
633         if (j.has("rangingRoundUsage")) {
634             builder.setRangingRoundUsage(j.getInt("rangingRoundUsage"));
635         }
636         if (j.has("multiNodeMode")) {
637             builder.setMultiNodeMode(j.getInt("multiNodeMode"));
638         }
639         if (j.has("deviceAddress")) {
640             JSONArray jArray = j.getJSONArray("deviceAddress");
641             byte[] bArray = convertJSONArrayToByteArray(jArray);
642             UwbAddress deviceAddress = getComputedMacAddress(UwbAddress.fromBytes(bArray));
643             builder.setDeviceAddress(deviceAddress);
644         }
645         if (j.has("destinationAddresses")) {
646             JSONArray jArray = j.getJSONArray("destinationAddresses");
647             UwbAddress[] destinationUwbAddresses = new UwbAddress[jArray.length()];
648             for (int i = 0; i < jArray.length(); i++) {
649                 destinationUwbAddresses[i] = getComputedMacAddress(UwbAddress.fromBytes(
650                         convertJSONArrayToByteArray(jArray.getJSONArray(i))));
651             }
652             builder.setDestAddressList(Arrays.asList(destinationUwbAddresses));
653         }
654         if (j.has("initiationTimeMs")) {
655             builder.setInitiationTime(j.getInt("initiationTimeMs"));
656         }
657         if (j.has("slotDurationRstu")) {
658             builder.setSlotDurationRstu(j.getInt("slotDurationRstu"));
659         }
660         if (j.has("slotsPerRangingRound")) {
661             builder.setSlotsPerRangingRound(j.getInt("slotsPerRangingRound"));
662         }
663         if (j.has("rangingIntervalMs")) {
664             builder.setRangingIntervalMs(j.getInt("rangingIntervalMs"));
665         }
666         if (j.has("blockStrideLength")) {
667             builder.setBlockStrideLength(j.getInt("blockStrideLength"));
668         }
669         if (j.has("hoppingMode")) {
670             builder.setHoppingMode(j.getInt("hoppingMode"));
671         }
672         if (j.has("maxRangingRoundRetries")) {
673             builder.setMaxRangingRoundRetries(j.getInt("maxRangingRoundRetries"));
674         }
675         if (j.has("maxNumberOfMeasurements")) {
676             builder.setMaxNumberOfMeasurements(j.getInt("maxNumberOfMeasurements"));
677         }
678         if (j.has("sessionPriority")) {
679             builder.setSessionPriority(j.getInt("sessionPriority"));
680         }
681         if (j.has("macAddressMode")) {
682             builder.setMacAddressMode(j.getInt("macAddressMode"));
683         }
684         if (j.has("inBandTerminationAttemptCount")) {
685             builder.setInBandTerminationAttemptCount(j.getInt("inBandTerminationAttemptCount"));
686         }
687         if (j.has("channel")) {
688             builder.setChannelNumber(j.getInt("channel"));
689         }
690         if (j.has("preamble")) {
691             builder.setPreambleCodeIndex(j.getInt("preamble"));
692         }
693         if (j.getInt("stsConfig") == FiraParams.STS_CONFIG_STATIC) {
694             JSONArray jVendorIdArray = j.getJSONArray("vendorId");
695             builder.setVendorId(getComputedVendorId(convertJSONArrayToByteArray(jVendorIdArray)));
696             JSONArray jStatisStsIVArray = j.getJSONArray("staticStsIV");
697             builder.setStaticStsIV(convertJSONArrayToByteArray(jStatisStsIVArray));
698         } else if (j.getInt("stsConfig") == FiraParams.STS_CONFIG_PROVISIONED) {
699             builder.setStsConfig(j.getInt("stsConfig"));
700             JSONArray jSessionKeyArray = j.getJSONArray("sessionKey");
701             builder.setSessionKey(convertJSONArrayToByteArray(jSessionKeyArray));
702         } else if (j.getInt(
703                 "stsConfig") == FiraParams.STS_CONFIG_PROVISIONED_FOR_CONTROLEE_INDIVIDUAL_KEY) {
704             builder.setStsConfig(j.getInt("stsConfig"));
705             JSONArray jSessionKeyArray = j.getJSONArray("sessionKey");
706             builder.setSessionKey(convertJSONArrayToByteArray(jSessionKeyArray));
707             if (j.getInt("deviceType") == FiraParams.RANGING_DEVICE_TYPE_CONTROLEE) {
708                 JSONArray jSubSessionKeyArray = j.getJSONArray("subSessionKey");
709                 builder.setSubsessionKey(convertJSONArrayToByteArray(jSubSessionKeyArray));
710                 builder.setSubSessionId(j.getInt("subSessionId"));
711             }
712         }
713         if (j.has("aoaResultRequest")) {
714             builder.setAoaResultRequest(j.getInt("aoaResultRequest"));
715         }
716         if (j.has("filterType")) {
717             builder.setFilterType(j.getInt("filterType"));
718         }
719         if (j.has("rangeDataNtfConfig")) {
720             builder.setRangeDataNtfConfig(j.getInt("rangeDataNtfConfig"));
721         }
722         if (j.has("errorStreakTimeoutInMs")) {
723             builder.setRangingErrorStreakTimeoutMs(j.getInt("errorStreakTimeoutInMs"));
724         }
725         if (j.has("hasRangingResultReportMessage")) {
726             builder.setHasRangingResultReportMessage(j.getBoolean("hasRangingResultReportMessage"));
727         }
728         if (j.has("rangingErrorStreakTimeoutMs")) {
729             builder.setRangingErrorStreakTimeoutMs(j.getLong("rangingErrorStreakTimeoutMs"));
730         }
731         if (j.has("isKeyRotationEnabled")) {
732             builder.setIsKeyRotationEnabled(j.getBoolean("isKeyRotationEnabled"));
733         }
734         if (j.has("keyRotationRate")) {
735             builder.setKeyRotationRate(j.getInt("keyRotationRate"));
736         }
737         if (j.has("hasRangingResultReportMessage")) {
738             builder.setHasRangingResultReportMessage(j.getBoolean("hasRangingResultReportMessage"));
739         }
740         if (j.has("prfMode")) {
741             builder.setPrfMode(j.getInt("prfMode"));
742         }
743 
744         return builder.build();
745     }
746 
getRangingMeasurement(String key, JSONArray jArray)747     private RangingMeasurement getRangingMeasurement(String key, JSONArray jArray)
748             throws JSONException {
749         byte[] bArray = convertJSONArrayToByteArray(jArray);
750         UwbAddress peerAddress = getComputedMacAddress(UwbAddress.fromBytes(bArray));
751         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
752         List<RangingMeasurement> rangingMeasurements =
753                 rangingSessionCallback.rangingReport.getMeasurements();
754         for (RangingMeasurement r: rangingMeasurements) {
755             if (r.getStatus() == RangingMeasurement.RANGING_STATUS_SUCCESS
756                     && r.getRemoteDeviceAddress().equals(peerAddress)) {
757                 Log.d(TAG + "Found peer " + peerAddress.toString());
758                 return r;
759             }
760         }
761         Log.w(TAG + "Invalid ranging status or peer not found.");
762         return null;
763     }
764 
765     /** Open FIRA UWB ranging session. */
766     @AsyncRpc(description = "Open FIRA UWB ranging session")
openFiraRangingSession(String callbackId, String key, JSONObject config)767     public void openFiraRangingSession(String callbackId, String key, JSONObject config)
768             throws Throwable {
769         RangingSessionCallback rangingSessionCallback = new RangingSessionCallback(
770                 callbackId, Event.EventAll.getType());
771         FiraOpenSessionParams params = generateFiraOpenSessionParams(config);
772         runWithShellPermission(() ->
773                 mUwbManager.openRangingSession(
774                         params.toBundle(), mExecutor, rangingSessionCallback));
775         sRangingSessionCallbackMap.put(key, rangingSessionCallback);
776     }
777 
778     /** Open CCC UWB ranging session. */
779     @AsyncRpc(description = "Open CCC UWB ranging session")
openCccRangingSession(String callbackId, String key, JSONObject config)780     public void openCccRangingSession(String callbackId, String key, JSONObject config)
781             throws Throwable {
782         RangingSessionCallback rangingSessionCallback = new RangingSessionCallback(
783                 callbackId, Event.EventAll.getType());
784         CccOpenRangingParams params = generateCccOpenRangingParams(config);
785         runWithShellPermission(() ->
786                 mUwbManager.openRangingSession(
787                         params.toBundle(), mExecutor, rangingSessionCallback));
788         sRangingSessionCallbackMap.put(key, rangingSessionCallback);
789     }
790 
791     /** Start FIRA UWB ranging. */
792     @Rpc(description = "Start FIRA UWB ranging")
startFiraRangingSession(String key)793     public void startFiraRangingSession(String key) throws Throwable {
794         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
795         // Hold on to the shell permission until the session is stopped to get the ranging reports.
796         adoptShellPermission();
797         rangingSessionCallback.rangingSession.start(new PersistableBundle());
798     }
799 
800     /** Start CCC UWB ranging. */
801     @Rpc(description = "Start CCC UWB ranging")
startCccRangingSession(String key, JSONObject config)802     public void startCccRangingSession(String key, JSONObject config) throws Throwable {
803         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
804         CccRangingStartedParams params = generateCccRangingStartedParams(config);
805         // Hold on to the shell permission until the session is stopped to get the ranging reports.
806         adoptShellPermission();
807         rangingSessionCallback.rangingSession.start(params.toBundle());
808     }
809 
810     /** Reconfigures FIRA UWB ranging session. */
811     @Rpc(description = "Reconfigure FIRA UWB ranging session")
reconfigureFiraRangingSession(String key, JSONObject config)812     public void reconfigureFiraRangingSession(String key, JSONObject config) throws Throwable {
813         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
814         FiraRangingReconfigureParams params = generateFiraRangingReconfigureParams(config);
815         // Hold on to the shell permission until the session is stopped to get the ranging reports.
816         adoptShellPermission();
817         rangingSessionCallback.rangingSession.reconfigure(params.toBundle());
818     }
819 
820     /** Reconfigures FIRA UWB ranging session to add controlee. */
821     @Rpc(description = "Reconfigure FIRA UWB ranging session to add controlee")
addControleeFiraRangingSession(String key, JSONObject config)822     public void addControleeFiraRangingSession(String key, JSONObject config) throws Throwable {
823         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
824         FiraControleeParams params = generateFiraControleeParams(config);
825         // Hold on to the shell permission until the session is stopped to get the ranging reports.
826         adoptShellPermission();
827         rangingSessionCallback.rangingSession.addControlee(params.toBundle());
828     }
829 
830     /** Reconfigures FIRA UWB ranging session to remove controlee. */
831     @Rpc(description = "Reconfigure FIRA UWB ranging session to remove controlee")
removeControleeFiraRangingSession(String key, JSONObject config)832     public void removeControleeFiraRangingSession(String key, JSONObject config)
833             throws Throwable {
834         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
835         FiraControleeParams params = generateFiraControleeParams(config);
836         // Hold on to the shell permission until the session is stopped to get the ranging reports.
837         adoptShellPermission();
838         rangingSessionCallback.rangingSession.removeControlee(params.toBundle());
839     }
840 
841     /**
842      * Find if UWB peer is found.
843      */
844     @Rpc(description = "Find if UWB peer is found")
isUwbPeerFound(String key, JSONArray jArray)845     public boolean isUwbPeerFound(String key, JSONArray jArray) throws JSONException {
846         return getRangingMeasurement(key, jArray) != null;
847     }
848 
849     /** Get UWB distance measurement. */
850     @Rpc(description = "Get UWB ranging distance measurement with peer.")
getDistanceMeasurement(String key, JSONArray jArray)851     public double getDistanceMeasurement(String key, JSONArray jArray) throws JSONException {
852         RangingMeasurement rangingMeasurement = getRangingMeasurement(key, jArray);
853         if (rangingMeasurement == null || rangingMeasurement.getDistanceMeasurement() == null) {
854             throw new NullPointerException("Cannot get Distance Measurement on null object.");
855         }
856         return rangingMeasurement.getDistanceMeasurement().getMeters();
857     }
858 
859     /** Get angle of arrival azimuth measurement. */
860     @Rpc(description = "Get UWB AoA Azimuth measurement.")
getAoAAzimuthMeasurement(String key, JSONArray jArray)861     public double getAoAAzimuthMeasurement(String key, JSONArray jArray) throws JSONException {
862         RangingMeasurement rangingMeasurement = getRangingMeasurement(key, jArray);
863         if (rangingMeasurement == null
864                 || rangingMeasurement.getAngleOfArrivalMeasurement() == null
865                 || rangingMeasurement.getAngleOfArrivalMeasurement().getAzimuth() == null) {
866             throw new NullPointerException("Cannot get AoA azimuth measurement on null object.");
867         }
868         return rangingMeasurement.getAngleOfArrivalMeasurement().getAzimuth().getRadians();
869     }
870 
871     /** Get angle of arrival altitude measurement. */
872     @Rpc(description = "Get UWB AoA Altitude measurement.")
getAoAAltitudeMeasurement(String key, JSONArray jArray)873     public double getAoAAltitudeMeasurement(String key, JSONArray jArray) throws JSONException {
874         RangingMeasurement rangingMeasurement = getRangingMeasurement(key, jArray);
875         if (rangingMeasurement == null
876                 || rangingMeasurement.getAngleOfArrivalMeasurement() == null
877                 || rangingMeasurement.getAngleOfArrivalMeasurement().getAltitude() == null) {
878             throw new NullPointerException("Cannot get AoA altitude measurement on null object.");
879         }
880         return rangingMeasurement.getAngleOfArrivalMeasurement().getAltitude().getRadians();
881     }
882 
883     /** Get RSSI measurement. */
884     @Rpc(description = "Get RSSI measurement.")
getRssiDbmMeasurement(String key, JSONArray jArray)885     public int getRssiDbmMeasurement(String key, JSONArray jArray) throws JSONException {
886         RangingMeasurement rangingMeasurement = getRangingMeasurement(key, jArray);
887         if (rangingMeasurement == null) {
888             throw new NullPointerException("Cannot get RSSI dBm measurement on null object.");
889         }
890         return rangingMeasurement.getRssiDbm();
891     }
892 
893     /** Stop UWB ranging. */
894     @Rpc(description = "Stop UWB ranging")
stopRangingSession(String key)895     public void stopRangingSession(String key) throws Throwable {
896         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key);
897         runWithShellPermission(() ->
898                 rangingSessionCallback.rangingSession.stop());
899     }
900 
901     /** Close UWB ranging session. */
902     @Rpc(description = "Close UWB ranging session")
closeRangingSession(String key)903     public void closeRangingSession(String key) throws Throwable {
904         RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.remove(key);
905         if (rangingSessionCallback != null && rangingSessionCallback.rangingSession != null) {
906             runWithShellPermission(() ->
907                     rangingSessionCallback.rangingSession.close());
908         }
909     }
910 
convertPersistableBundleToJson(PersistableBundle bundle)911     private JSONObject convertPersistableBundleToJson(PersistableBundle bundle)
912             throws JSONException {
913         JSONObject jsonObj = new JSONObject();
914         Set<String> keys = bundle.keySet();
915         for (String key: keys) {
916             if (bundle.get(key) instanceof PersistableBundle) {
917                 jsonObj.put(key, convertPersistableBundleToJson(
918                         (PersistableBundle) bundle.get(key)));
919             } else {
920                 jsonObj.put(key, JSONObject.wrap(bundle.get(key)));
921             }
922         }
923         return jsonObj;
924     }
925 
926     /** Get UWB specification info */
927     @Rpc(description = "Get Uwb specification info")
getSpecificationInfo()928     public JSONObject getSpecificationInfo() throws Throwable {
929         return runWithShellPermission(() ->
930                        convertPersistableBundleToJson(mUwbManager.getSpecificationInfo()));
931     }
932 
933     /** Set airplane mode to True or False */
934     @Rpc(description = "Set airplane mode")
setAirplaneMode(Boolean enabled)935     public void setAirplaneMode(Boolean enabled) throws Throwable {
936         runWithShellPermission(() -> mConnectivityManager.setAirplaneMode(enabled));
937     }
938 
939     @Rpc(description = "Log info level message to device logcat")
logInfo(String message)940     public void logInfo(String message) throws JSONException {
941         Log.i(TAG + message);
942     }
943 
944     @Override
shutdown()945     public void shutdown() {}
946 
adoptShellPermission()947     private void adoptShellPermission() {
948         UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
949         uia.adoptShellPermissionIdentity();
950         try {
951             Class<?> cls = Class.forName("android.app.UiAutomation");
952             Method destroyMethod = cls.getDeclaredMethod("destroy");
953             destroyMethod.invoke(uia);
954         } catch (ReflectiveOperationException e) {
955             throw new IllegalStateException("Failed to cleaup Ui Automation", e);
956         }
957     }
958 
dropShellPermission()959     private void dropShellPermission() {
960         UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
961         uia.dropShellPermissionIdentity();
962         try {
963             Class<?> cls = Class.forName("android.app.UiAutomation");
964             Method destroyMethod = cls.getDeclaredMethod("destroy");
965             destroyMethod.invoke(uia);
966         } catch (ReflectiveOperationException e) {
967             throw new IllegalStateException("Failed to cleaup Ui Automation", e);
968         }
969     }
970 
getReverseBytes(byte[] data)971     private static byte[] getReverseBytes(byte[] data) {
972         byte[] buffer = new byte[data.length];
973         for (int i = 0; i < data.length; i++) {
974             buffer[i] = data[data.length - 1 - i];
975         }
976         return buffer;
977     }
getComputedMacAddress(UwbAddress address)978     private static UwbAddress getComputedMacAddress(UwbAddress address) {
979         if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
980             return UwbAddress.fromBytes(getReverseBytes(address.toBytes()));
981         }
982         return address;
983     }
984 
getComputedVendorId(byte[] data)985     private static byte[] getComputedVendorId(byte[] data) {
986         if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
987             return getReverseBytes(data);
988         }
989         return data;
990     }
991 
runWithShellPermission(Runnable action)992     public void runWithShellPermission(Runnable action) throws Throwable {
993         adoptShellPermission();
994         try {
995             action.run();
996         } finally {
997             dropShellPermission();
998         }
999     }
1000 
runWithShellPermission(ThrowingSupplier<T> action)1001     public <T> T runWithShellPermission(ThrowingSupplier<T> action) throws Throwable {
1002         adoptShellPermission();
1003         try {
1004             return action.get();
1005         } finally {
1006             dropShellPermission();
1007         }
1008     }
1009 
1010     /**
1011      * Similar to {@link Supplier} but has {@code throws Exception}.
1012      *
1013      * @param <T> type of the value produced
1014      */
1015     public interface ThrowingSupplier<T> {
1016         /**
1017          * Similar to {@link Supplier#get} but has {@code throws Exception}.
1018          */
get()1019         T get() throws Exception;
1020     }
1021 }
1022