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 multidevices.snippet.ranging;
18 
19 import android.app.UiAutomation;
20 import android.content.Context;
21 import android.os.RemoteException;
22 import android.util.Log;
23 
24 import androidx.core.uwb.backend.impl.internal.RangingParameters;
25 import androidx.core.uwb.backend.impl.internal.UwbAddress;
26 import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
27 import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
28 import androidx.test.platform.app.InstrumentationRegistry;
29 
30 import com.android.ranging.generic.RangingTechnology;
31 import com.android.ranging.generic.ranging.PrecisionData;
32 import com.android.ranging.generic.ranging.PrecisionRanging;
33 import com.android.ranging.generic.ranging.PrecisionRangingConfig;
34 import com.android.ranging.generic.ranging.PrecisionRangingImpl;
35 import com.android.ranging.generic.ranging.UwbAdapter;
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.Rpc;
41 import com.google.common.collect.ImmutableList;
42 import com.google.common.collect.ImmutableMap;
43 import com.google.common.util.concurrent.ListeningExecutorService;
44 import com.google.common.util.concurrent.MoreExecutors;
45 
46 import dagger.Lazy;
47 
48 import org.json.JSONArray;
49 import org.json.JSONException;
50 import org.json.JSONObject;
51 
52 import java.lang.reflect.Method;
53 import java.time.Duration;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Optional;
59 import java.util.concurrent.Executors;
60 
61 public class GenericRangingSnippet implements Snippet {
62 
63     private static final String TAG = "GenericRangingSnippet: ";
64     private final Context mContext;
65     private final ListeningExecutorService mExecutor = MoreExecutors.listeningDecorator(
66             Executors.newSingleThreadExecutor());
67     private final EventCache mEventCache = EventCache.getInstance();
68     private static final HashMap<String, PrecisionRanging> sRangingHashMap =
69             new HashMap<>();
70 
GenericRangingSnippet()71     public GenericRangingSnippet() throws Throwable {
72         adoptShellPermission();
73         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
74     }
75 
76     private static class UwbManagerSnippetException extends Exception {
77 
UwbManagerSnippetException(String msg)78         UwbManagerSnippetException(String msg) {
79             super(msg);
80         }
81 
UwbManagerSnippetException(String msg, Throwable err)82         UwbManagerSnippetException(String msg, Throwable err) {
83             super(msg, err);
84         }
85     }
86 
adoptShellPermission()87     private void adoptShellPermission() throws Throwable {
88         UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
89         uia.adoptShellPermissionIdentity();
90         try {
91             Class<?> cls = Class.forName("android.app.UiAutomation");
92             Method destroyMethod = cls.getDeclaredMethod("destroy");
93             destroyMethod.invoke(uia);
94         } catch (ReflectiveOperationException e) {
95             throw new UwbManagerSnippetException("Failed to cleaup Ui Automation", e);
96         }
97     }
98 
99     private enum Event {
100         Invalid(0),
101         Started(1 << 0),
102         Stopped(1 << 1),
103         ReportReceived(1 << 2),
104         EventAll(
105                 1 << 0
106                         | 1 << 1
107                         | 1 << 2
108         );
109 
110         private final int mType;
111 
Event(int type)112         Event(int type) {
113             mType = type;
114         }
115 
getType()116         private int getType() {
117             return mType;
118         }
119     }
120 
121     class GenericRangingCallback implements PrecisionRanging.Callback {
122 
123         public String mId;
124 
GenericRangingCallback(String id, int events)125         GenericRangingCallback(String id, int events) {
126             mId = id;
127         }
128 
handleEvent(Event e)129         private void handleEvent(Event e) {
130             Log.d(TAG, "GenericRangingCallback#handleEvent() for " + e.toString());
131             SnippetEvent event = new SnippetEvent(mId, "GenericRangingCallback");
132             event.getData().putString("genericRangingSessionEvent", e.toString());
133             mEventCache.postEvent(event);
134         }
135 
136         @Override
onStarted()137         public void onStarted() {
138             Log.d(TAG, "GenericRangingCallback#onStarted() called");
139             handleEvent(Event.Started);
140         }
141 
142         @Override
onStopped(int reason)143         public void onStopped(int reason) {
144             Log.d(TAG, "GenericRangingCallback#onStopped() called");
145             handleEvent(Event.Stopped);
146         }
147 
148         @Override
onData(PrecisionData data)149         public void onData(PrecisionData data) {
150             Log.d(TAG, "GenericRangingCallback#onData() called");
151             handleEvent(Event.ReportReceived);
152         }
153     }
154 
generateRangingParameters(JSONObject j)155     private RangingParameters generateRangingParameters(JSONObject j) throws JSONException {
156         if (j == null) {
157             return null;
158         }
159         List<UwbAddress> peerAddresses = new ArrayList<>();
160         if (j.has("destinationAddresses")) {
161             JSONArray jArray = j.getJSONArray("destinationAddresses");
162             UwbAddress[] destinationUwbAddresses = new UwbAddress[jArray.length()];
163             for (int i = 0; i < jArray.length(); i++) {
164                 destinationUwbAddresses[i] = UwbAddress.fromBytes(
165                         convertJSONArrayToByteArray(jArray.getJSONArray(i)));
166             }
167             peerAddresses = Arrays.asList(destinationUwbAddresses);
168         }
169         UwbComplexChannel uwbComplexChannel = new UwbComplexChannel(9, 11);
170         UwbRangeDataNtfConfig rangeDataNtfConfig = new UwbRangeDataNtfConfig.Builder().build();
171 
172         return new RangingParameters(
173                 j.getInt("configId"),
174                 j.getInt("sessionId"),
175                 j.getInt("subSessionId"),
176                 convertJSONArrayToByteArray(j.getJSONArray("sessionKey")),
177                 null,
178                 uwbComplexChannel,
179                 peerAddresses,
180                 j.getInt("rangingUpdateRate"),
181                 rangeDataNtfConfig,
182                 j.getInt("slotDuration"),
183                 j.getBoolean("isAoaDisabled")
184         );
185     }
186 
convertJSONArrayToByteArray(JSONArray jArray)187     private byte[] convertJSONArrayToByteArray(JSONArray jArray) throws JSONException {
188         if (jArray == null) {
189             return null;
190         }
191         byte[] bArray = new byte[jArray.length()];
192         for (int i = 0; i < jArray.length(); i++) {
193             bArray[i] = (byte) jArray.getInt(i);
194         }
195         return bArray;
196     }
197 
198     @Rpc(description = "Start UWB ranging session")
startUwbRanging(String key, JSONObject config)199     public void startUwbRanging(String key, JSONObject config)
200             throws JSONException, RemoteException {
201         int deviceType = config.getInt("deviceType");
202         UwbAdapter uwbAdapter = null;
203         if (deviceType == 0) {
204             logInfo("Starting controlee session");
205             uwbAdapter = new UwbAdapter(mContext, mExecutor, UwbAdapter.DeviceType.CONTROLEE);
206         } else {
207             logInfo("Starting controller session");
208             uwbAdapter = new UwbAdapter(mContext, mExecutor, UwbAdapter.DeviceType.CONTROLLER);
209         }
210 
211         //TODO: Make this configurable
212         //    private Provider<PrecisionRanging.Factory> mRangingFactory;
213         PrecisionRangingConfig precisionRangingConfig =
214                 PrecisionRangingConfig.builder().setRangingTechnologiesToRangeWith(
215                         ImmutableList.of(RangingTechnology.UWB)).setUseFusingAlgorithm(
216                         false).setMaxUpdateInterval(
217                         Duration.ofMillis(200)).setFusionAlgorithmDriftTimeout(
218                         Duration.ofSeconds(1)).setNoUpdateTimeout(
219                         Duration.ofSeconds(2)).setInitTimeout(Duration.ofSeconds(3)).build();
220 
221         PrecisionRanging precisionRanging = new PrecisionRangingImpl(
222                 new CustomUwbAdapterProvider(uwbAdapter), mContext, precisionRangingConfig,
223                 Executors.newSingleThreadScheduledExecutor(),
224                 Optional.of(ImmutableMap.of(RangingTechnology.UWB, uwbAdapter)));
225 
226         precisionRanging.setUwbConfig(generateRangingParameters(config));
227         uwbAdapter.setLocalADdress(UwbAddress.fromBytes(
228                 convertJSONArrayToByteArray(config.getJSONArray("deviceAddress"))));
229 
230         // Test forces channel to 9 and preamble to 11
231         uwbAdapter.setComplexChannelForTesting();
232         precisionRanging.getUwbComplexChannel();
233         GenericRangingCallback genericRangingCallback = new GenericRangingCallback("1",
234                 Event.EventAll.getType());
235         sRangingHashMap.put(key, precisionRanging);
236         precisionRanging.start(genericRangingCallback);
237     }
238 
239     @Rpc(description = "Start UWB ranging session")
stopUwbRanging(String key)240     public void stopUwbRanging(String key) throws JSONException {
241         if (sRangingHashMap.containsKey(key)) {
242             sRangingHashMap.get(key).stop();
243         }
244     }
245 
246     @Rpc(description = "Log info level message to device logcat")
logInfo(String message)247     public void logInfo(String message) throws JSONException {
248         com.google.android.mobly.snippet.util.Log.i(TAG + message);
249     }
250 
251     private static class CustomUwbAdapterProvider implements Lazy<UwbAdapter> {
252         private final UwbAdapter mUwbAdapter;
253 
CustomUwbAdapterProvider(UwbAdapter uwbAdapter)254         CustomUwbAdapterProvider(UwbAdapter uwbAdapter) {
255             this.mUwbAdapter = uwbAdapter;
256         }
257 
258         @Override
get()259         public UwbAdapter get() {
260             return mUwbAdapter;
261         }
262     }
263 }
264 
265