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