1 /* 2 * Copyright (C) 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 androidx.core.uwb.backend.impl.internal; 18 19 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_FAILED_TO_START; 20 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_STOP_RANGING_CALLED; 21 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_WRONG_PARAMETERS; 22 import static androidx.core.uwb.backend.impl.internal.Utils.INVALID_API_CALL; 23 import static androidx.core.uwb.backend.impl.internal.Utils.RANGING_ALREADY_STARTED; 24 import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK; 25 import static androidx.core.uwb.backend.impl.internal.Utils.TAG; 26 import static androidx.core.uwb.backend.impl.internal.Utils.UWB_RECONFIGURATION_FAILURE; 27 import static androidx.core.uwb.backend.impl.internal.Utils.UWB_SYSTEM_CALLBACK_FAILURE; 28 29 import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_DT_TAG; 30 31 import static java.util.Objects.requireNonNull; 32 33 import android.os.Build.VERSION; 34 import android.os.Build.VERSION_CODES; 35 import android.os.PersistableBundle; 36 import android.util.Log; 37 import android.uwb.RangingMeasurement; 38 import android.uwb.RangingReport; 39 import android.uwb.RangingSession; 40 import android.uwb.UwbManager; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.annotation.WorkerThread; 45 46 import com.google.common.hash.Hashing; 47 import com.google.uwb.support.dltdoa.DlTDoARangingRoundsUpdate; 48 import com.google.uwb.support.fira.FiraOpenSessionParams; 49 import com.google.uwb.support.multichip.ChipInfoParams; 50 51 import java.util.Arrays; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.concurrent.Executor; 55 import java.util.concurrent.ExecutorService; 56 57 /** Implements start/stop ranging operations. */ 58 public abstract class RangingDevice { 59 60 public static final int SESSION_ID_UNSET = 0; 61 private static final String NO_MULTICHIP_SUPPORT = "NO_MULTICHIP_SUPPORT"; 62 63 /** Timeout value after ranging start call */ 64 private static final int RANGING_START_TIMEOUT_MILLIS = 3100; 65 66 protected final UwbManager mUwbManager; 67 68 private final OpAsyncCallbackRunner<Boolean> mOpAsyncCallbackRunner; 69 70 @Nullable 71 private UwbAddress mLocalAddress; 72 73 @Nullable 74 protected UwbComplexChannel mComplexChannel; 75 76 @Nullable 77 protected RangingParameters mRangingParameters; 78 79 /** A serial thread used by System API to handle session callbacks. */ 80 private Executor mSystemCallbackExecutor; 81 82 /** A serial thread used in system API callbacks to handle Backend callbacks */ 83 @Nullable 84 private ExecutorService mBackendCallbackExecutor; 85 86 /** NotNull when session opening is successful. Set to Null when session is closed. */ 87 @Nullable 88 private RangingSession mRangingSession; 89 90 private boolean mIsRanging = false; 91 92 /** If true, local address and complex channel will be hardcoded */ 93 private Boolean mForTesting = false; 94 95 @Nullable 96 private RangingRoundFailureCallback mRangingRoundFailureCallback = null; 97 98 private boolean mRangingReportedAllowed = false; 99 100 @Nullable 101 private String mChipId = null; 102 103 @NonNull 104 protected final UwbFeatureFlags mUwbFeatureFlags; 105 106 private final HashMap<String, UwbAddress> mMultiChipMap; 107 RangingDevice(UwbManager manager, Executor executor, OpAsyncCallbackRunner<Boolean> opAsyncCallbackRunner, UwbFeatureFlags uwbFeatureFlags)108 RangingDevice(UwbManager manager, Executor executor, 109 OpAsyncCallbackRunner<Boolean> opAsyncCallbackRunner, UwbFeatureFlags uwbFeatureFlags) { 110 mUwbManager = manager; 111 this.mSystemCallbackExecutor = executor; 112 mOpAsyncCallbackRunner = opAsyncCallbackRunner; 113 mOpAsyncCallbackRunner.setOperationTimeoutMillis(RANGING_START_TIMEOUT_MILLIS); 114 mUwbFeatureFlags = uwbFeatureFlags; 115 this.mMultiChipMap = new HashMap<>(); 116 initializeUwbAddress(); 117 } 118 119 /** Sets the chip ID. By default, the default chip is used. */ setChipId(String chipId)120 public void setChipId(String chipId) { 121 mChipId = chipId; 122 } 123 isForTesting()124 public Boolean isForTesting() { 125 return mForTesting; 126 } 127 setForTesting(Boolean forTesting)128 public void setForTesting(Boolean forTesting) { 129 mForTesting = forTesting; 130 } 131 132 /** Gets local address. The first call will return a randomized short address. */ getLocalAddress()133 public UwbAddress getLocalAddress() { 134 if (isLocalAddressSet()) { 135 return mLocalAddress; 136 } 137 // UwbManager#getDefaultChipId is supported from Android T. 138 if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) { 139 return getLocalAddress(NO_MULTICHIP_SUPPORT); 140 } 141 String defaultChipId = mUwbManager.getDefaultChipId(); 142 return getLocalAddress(defaultChipId); 143 } 144 145 /** Gets local address given chip ID. The first call will return a randomized short address. */ getLocalAddress(String chipId)146 public UwbAddress getLocalAddress(String chipId) { 147 if (mMultiChipMap.get(chipId) == null) { 148 mMultiChipMap.put(chipId, getRandomizedLocalAddress()); 149 } 150 mLocalAddress = mMultiChipMap.get(chipId); 151 return mLocalAddress; 152 } 153 154 /** Check whether local address was previously set. */ isLocalAddressSet()155 public boolean isLocalAddressSet() { 156 return mLocalAddress != null; 157 } 158 159 /** Sets local address. */ setLocalAddress(UwbAddress localAddress)160 public void setLocalAddress(UwbAddress localAddress) { 161 mLocalAddress = localAddress; 162 } 163 164 /** Gets a randomized short address. */ getRandomizedLocalAddress()165 private UwbAddress getRandomizedLocalAddress() { 166 return UwbAddress.getRandomizedShortAddress(); 167 } 168 hashSessionId(RangingParameters rangingParameters)169 protected abstract int hashSessionId(RangingParameters rangingParameters); 170 calculateHashedSessionId( UwbAddress controllerAddress, UwbComplexChannel complexChannel)171 static int calculateHashedSessionId( 172 UwbAddress controllerAddress, UwbComplexChannel complexChannel) { 173 return Hashing.sha256() 174 .newHasher() 175 .putBytes(controllerAddress.toBytes()) 176 .putInt(complexChannel.encode()) 177 .hash() 178 .asInt(); 179 } 180 181 /** Sets the ranging parameter for this session. */ setRangingParameters(RangingParameters rangingParameters)182 public synchronized void setRangingParameters(RangingParameters rangingParameters) { 183 if (rangingParameters.getSessionId() == SESSION_ID_UNSET) { 184 int sessionId = hashSessionId(rangingParameters); 185 mRangingParameters = 186 new RangingParameters( 187 rangingParameters.getUwbConfigId(), 188 sessionId, 189 rangingParameters.getSubSessionId(), 190 rangingParameters.getSessionKeyInfo(), 191 rangingParameters.getSubSessionKeyInfo(), 192 rangingParameters.getComplexChannel(), 193 rangingParameters.getPeerAddresses(), 194 rangingParameters.getRangingUpdateRate(), 195 rangingParameters.getUwbRangeDataNtfConfig(), 196 rangingParameters.getSlotDuration(), 197 rangingParameters.isAoaDisabled()); 198 } else { 199 mRangingParameters = rangingParameters; 200 } 201 } 202 203 /** Alive means the session is open. */ isAlive()204 public boolean isAlive() { 205 return mRangingSession != null; 206 } 207 208 /** 209 * Is the ranging ongoing or not. Since the device can be stopped by peer or scheduler, the 210 * session can be open but not ranging 211 */ isRanging()212 public boolean isRanging() { 213 return mIsRanging; 214 } 215 isKnownPeer(UwbAddress address)216 protected boolean isKnownPeer(UwbAddress address) { 217 requireNonNull(mRangingParameters); 218 return mRangingParameters.getPeerAddresses().contains(address); 219 } 220 221 /** 222 * Converts the {@link RangingReport} to {@link RangingPosition} and invokes the GMSCore 223 * callback. 224 */ 225 // Null-guard prevents this from being null onRangingDataReceived( RangingReport rangingReport, RangingSessionCallback callback)226 private synchronized void onRangingDataReceived( 227 RangingReport rangingReport, RangingSessionCallback callback) { 228 List<RangingMeasurement> measurements = rangingReport.getMeasurements(); 229 for (RangingMeasurement measurement : measurements) { 230 byte[] remoteAddressBytes = measurement.getRemoteDeviceAddress().toBytes(); 231 if (mUwbFeatureFlags.isReversedByteOrderFiraParams()) { 232 remoteAddressBytes = Conversions.getReverseBytes(remoteAddressBytes); 233 } 234 235 236 UwbAddress peerAddress = UwbAddress.fromBytes(remoteAddressBytes); 237 if (!isKnownPeer(peerAddress) && !Conversions.isDlTdoaMeasurement(measurement)) { 238 Log.w(TAG, 239 String.format("Received ranging data from unknown peer %s.", peerAddress)); 240 continue; 241 } 242 243 if (measurement.getStatus() != RangingMeasurement.RANGING_STATUS_SUCCESS 244 && mRangingRoundFailureCallback != null) { 245 mRangingRoundFailureCallback.onRangingRoundFailed(peerAddress); 246 } 247 248 RangingPosition currentPosition = Conversions.convertToPosition(measurement); 249 if (currentPosition == null) { 250 continue; 251 } 252 UwbDevice uwbDevice = UwbDevice.createForAddress(peerAddress.toBytes()); 253 callback.onRangingResult(uwbDevice, currentPosition); 254 } 255 } 256 257 /** 258 * Run callbacks in {@link RangingSessionCallback} on this thread. Make sure that no lock is 259 * acquired when the callbacks are called since the code is out of this class. 260 */ runOnBackendCallbackThread(Runnable action)261 protected void runOnBackendCallbackThread(Runnable action) { 262 requireNonNull(mBackendCallbackExecutor); 263 mBackendCallbackExecutor.execute(action); 264 } 265 getUwbDevice()266 private UwbDevice getUwbDevice() { 267 return UwbDevice.createForAddress(getLocalAddress().toBytes()); 268 } 269 initializeUwbAddress()270 private void initializeUwbAddress() { 271 // UwbManager#getChipInfos is supported from Android T. 272 if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { 273 List<PersistableBundle> chipInfoBundles = mUwbManager.getChipInfos(); 274 for (PersistableBundle chipInfo : chipInfoBundles) { 275 mMultiChipMap.put(ChipInfoParams.fromBundle(chipInfo).getChipId(), 276 getRandomizedLocalAddress()); 277 } 278 } else { 279 mMultiChipMap.put(NO_MULTICHIP_SUPPORT, getRandomizedLocalAddress()); 280 } 281 } 282 convertCallback(RangingSessionCallback callback)283 protected RangingSession.Callback convertCallback(RangingSessionCallback callback) { 284 return new RangingSession.Callback() { 285 286 @WorkerThread 287 @Override 288 public void onOpened(RangingSession session) { 289 mRangingSession = session; 290 mOpAsyncCallbackRunner.complete(true); 291 } 292 293 @WorkerThread 294 @Override 295 public void onOpenFailed(int reason, PersistableBundle params) { 296 Log.i(TAG, String.format("Session open failed: reason %s", reason)); 297 int suspendedReason = Conversions.convertReason(reason); 298 if (suspendedReason == REASON_UNKNOWN) { 299 suspendedReason = REASON_FAILED_TO_START; 300 } 301 int finalSuspendedReason = suspendedReason; 302 runOnBackendCallbackThread( 303 () -> callback.onRangingSuspended(getUwbDevice(), finalSuspendedReason)); 304 mRangingSession = null; 305 mOpAsyncCallbackRunner.complete(false); 306 } 307 308 @WorkerThread 309 @Override 310 public void onStarted(PersistableBundle sessionInfo) { 311 callback.onRangingInitialized(getUwbDevice()); 312 mIsRanging = true; 313 mOpAsyncCallbackRunner.complete(true); 314 } 315 316 @WorkerThread 317 @Override 318 public void onStartFailed(int reason, PersistableBundle params) { 319 320 int suspendedReason = Conversions.convertReason(reason); 321 if (suspendedReason != REASON_WRONG_PARAMETERS) { 322 suspendedReason = REASON_FAILED_TO_START; 323 } 324 int finalSuspendedReason = suspendedReason; 325 runOnBackendCallbackThread( 326 () -> callback.onRangingSuspended(getUwbDevice(), finalSuspendedReason)); 327 if (mRangingSession != null) { 328 mRangingSession.close(); 329 } 330 mRangingSession = null; 331 mOpAsyncCallbackRunner.complete(false); 332 } 333 334 @WorkerThread 335 @Override 336 public void onReconfigured(PersistableBundle params) { 337 mOpAsyncCallbackRunner.completeIfActive(true); 338 } 339 340 @WorkerThread 341 @Override 342 public void onReconfigureFailed(int reason, PersistableBundle params) { 343 mOpAsyncCallbackRunner.completeIfActive(false); 344 } 345 346 @WorkerThread 347 @Override 348 public void onStopped(int reason, PersistableBundle params) { 349 int suspendedReason = Conversions.convertReason(reason); 350 UwbDevice device = getUwbDevice(); 351 runOnBackendCallbackThread( 352 () -> { 353 synchronized (RangingDevice.this) { 354 mIsRanging = false; 355 } 356 callback.onRangingSuspended(device, suspendedReason); 357 }); 358 if (suspendedReason == REASON_STOP_RANGING_CALLED 359 && mOpAsyncCallbackRunner.isActive()) { 360 mOpAsyncCallbackRunner.complete(true); 361 } 362 } 363 364 @WorkerThread 365 @Override 366 public void onStopFailed(int reason, PersistableBundle params) { 367 mOpAsyncCallbackRunner.completeIfActive(false); 368 } 369 370 @WorkerThread 371 @Override 372 public void onClosed(int reason, PersistableBundle parameters) { 373 mRangingSession = null; 374 mOpAsyncCallbackRunner.completeIfActive(true); 375 } 376 377 @WorkerThread 378 @Override 379 public void onReportReceived(RangingReport rangingReport) { 380 if (mRangingReportedAllowed) { 381 runOnBackendCallbackThread( 382 () -> onRangingDataReceived(rangingReport, callback)); 383 } 384 } 385 386 @WorkerThread 387 @Override 388 public void onRangingRoundsUpdateDtTagStatus(PersistableBundle params) { 389 // Failure to set ranging rounds is not handled. 390 mOpAsyncCallbackRunner.complete(true); 391 } 392 393 @WorkerThread 394 @Override 395 public void onControleeAdded(PersistableBundle params) { 396 mOpAsyncCallbackRunner.complete(true); 397 } 398 399 @WorkerThread 400 @Override 401 public void onControleeAddFailed(int reason, PersistableBundle params) { 402 mOpAsyncCallbackRunner.complete(false); 403 } 404 405 @WorkerThread 406 @Override 407 public void onControleeRemoved(PersistableBundle params) { 408 if (mOpAsyncCallbackRunner.isActive()) { 409 mOpAsyncCallbackRunner.complete(true); 410 } 411 } 412 413 @WorkerThread 414 @Override 415 public void onControleeRemoveFailed(int reason, PersistableBundle params) { 416 mOpAsyncCallbackRunner.complete(false); 417 } 418 }; 419 } 420 421 protected abstract FiraOpenSessionParams getOpenSessionParams(); 422 423 private String getString(@Nullable Object o) { 424 if (o == null) { 425 return "null"; 426 } 427 if (o instanceof int[]) { 428 return Arrays.toString((int[]) o); 429 } 430 431 if (o instanceof byte[]) { 432 return Arrays.toString((byte[]) o); 433 } 434 435 if (o instanceof long[]) { 436 return Arrays.toString((long[]) o); 437 } 438 439 return o.toString(); 440 } 441 442 private void printStartRangingParameters(PersistableBundle parameters) { 443 Log.i(TAG, "Opens UWB session with bundle parameters:"); 444 for (String key : parameters.keySet()) { 445 Log.i(TAG, String.format( 446 "UWB parameter: %s, value: %s", key, getString(parameters.get(key)))); 447 } 448 } 449 450 /** 451 * Starts ranging. if an active ranging session exists, return {@link 452 * RangingSessionCallback#REASON_FAILED_TO_START} 453 */ 454 @Utils.UwbStatusCodes 455 public synchronized int startRanging( 456 RangingSessionCallback callback, ExecutorService backendCallbackExecutor) { 457 if (isAlive()) { 458 return RANGING_ALREADY_STARTED; 459 } 460 461 if (getLocalAddress() == null) { 462 return INVALID_API_CALL; 463 } 464 465 FiraOpenSessionParams openSessionParams = getOpenSessionParams(); 466 printStartRangingParameters(openSessionParams.toBundle()); 467 mBackendCallbackExecutor = backendCallbackExecutor; 468 boolean success = 469 mOpAsyncCallbackRunner.execOperation( 470 () -> { 471 if (mChipId != null) { 472 mUwbManager.openRangingSession( 473 openSessionParams.toBundle(), 474 mSystemCallbackExecutor, 475 convertCallback(callback), 476 mChipId); 477 } else { 478 mUwbManager.openRangingSession( 479 openSessionParams.toBundle(), 480 mSystemCallbackExecutor, 481 convertCallback(callback)); 482 } 483 }, 484 "Open session"); 485 486 Boolean result = mOpAsyncCallbackRunner.getResult(); 487 if (!success || result == null || !result) { 488 requireNonNull(mBackendCallbackExecutor); 489 mBackendCallbackExecutor.shutdown(); 490 mBackendCallbackExecutor = null; 491 // onRangingSuspended should have been called in the callback. 492 return STATUS_OK; 493 } 494 495 if (openSessionParams.getDeviceRole() == RANGING_DEVICE_DT_TAG) { 496 // Setting default ranging rounds value. 497 DlTDoARangingRoundsUpdate rangingRounds = 498 new DlTDoARangingRoundsUpdate.Builder() 499 .setSessionId(openSessionParams.getSessionId()) 500 .setNoOfRangingRounds(1) 501 .setRangingRoundIndexes(new byte[]{0}) 502 .build(); 503 success = 504 mOpAsyncCallbackRunner.execOperation( 505 () -> mRangingSession.updateRangingRoundsDtTag( 506 rangingRounds.toBundle()), 507 "Update ranging rounds for Dt Tag"); 508 } 509 510 success = 511 mOpAsyncCallbackRunner.execOperation( 512 () -> mRangingSession.start(new PersistableBundle()), "Start ranging"); 513 514 result = mOpAsyncCallbackRunner.getResult(); 515 requireNonNull(mBackendCallbackExecutor); 516 if (!success || result == null || !result) { 517 mBackendCallbackExecutor.shutdown(); 518 mBackendCallbackExecutor = null; 519 } else { 520 mRangingReportedAllowed = true; 521 } 522 return STATUS_OK; 523 } 524 525 /** Stops ranging if the session is ranging. */ 526 public synchronized int stopRanging() { 527 if (!isAlive()) { 528 Log.w(TAG, "UWB stopRanging called without an active session."); 529 return INVALID_API_CALL; 530 } 531 mRangingReportedAllowed = false; 532 if (mIsRanging) { 533 mOpAsyncCallbackRunner.execOperation( 534 () -> requireNonNull(mRangingSession).stop(), "Stop Ranging"); 535 } else { 536 Log.i(TAG, "UWB stopRanging called but isRanging is false."); 537 } 538 539 boolean success = 540 mOpAsyncCallbackRunner.execOperation( 541 () -> requireNonNull(mRangingSession).close(), "Close Session"); 542 543 if (mBackendCallbackExecutor != null) { 544 mBackendCallbackExecutor.shutdown(); 545 mBackendCallbackExecutor = null; 546 } 547 mLocalAddress = null; 548 mComplexChannel = null; 549 Boolean result = mOpAsyncCallbackRunner.getResult(); 550 if (!success || result == null || !result) { 551 return UWB_SYSTEM_CALLBACK_FAILURE; 552 } 553 return STATUS_OK; 554 } 555 556 /** 557 * Supports ranging configuration change. For example, a new peer is added to the active ranging 558 * session. 559 * 560 * @return returns true if the session is not active or reconfiguration is successful. 561 */ 562 protected synchronized boolean reconfigureRanging(PersistableBundle bundle) { 563 boolean success = 564 mOpAsyncCallbackRunner.execOperation( 565 () -> mRangingSession.reconfigure(bundle), "Reconfigure Ranging"); 566 Boolean result = mOpAsyncCallbackRunner.getResult(); 567 return success && result != null && result; 568 } 569 570 /** 571 * Adds a controlee to the active UWB ranging session. 572 * 573 * @return true if controlee was successfully added. 574 */ 575 protected synchronized boolean addControlee(PersistableBundle bundle) { 576 boolean success = 577 mOpAsyncCallbackRunner.execOperation( 578 () -> mRangingSession.addControlee(bundle), "Add controlee"); 579 Boolean result = mOpAsyncCallbackRunner.getResult(); 580 return success && result != null && result; 581 } 582 583 /** 584 * Removes a controlee from active UWB ranging session. 585 * 586 * @return true if controlee was successfully removed. 587 */ 588 protected synchronized boolean removeControlee(PersistableBundle bundle) { 589 boolean success = 590 mOpAsyncCallbackRunner.execOperation( 591 () -> mRangingSession.removeControlee(bundle), "Remove controlee"); 592 Boolean result = mOpAsyncCallbackRunner.getResult(); 593 return success && result != null && result; 594 } 595 596 597 /** 598 * Reconfigures range data notification for an ongoing session. 599 * 600 * @return STATUS_OK if reconfigure was successful. 601 * @return UWB_RECONFIGURATION_FAILURE if reconfigure failed. 602 * @return INVALID_API_CALL if ranging session is not active. 603 */ 604 public synchronized int reconfigureRangeDataNtfConfig(UwbRangeDataNtfConfig config) { 605 if (!isAlive()) { 606 Log.w(TAG, "Attempt to set range data notification while session is not active."); 607 return INVALID_API_CALL; 608 } 609 610 boolean success = 611 reconfigureRanging( 612 ConfigurationManager.createReconfigureParamsRangeDataNtf( 613 config).toBundle()); 614 615 if (!success) { 616 Log.w(TAG, "Reconfiguring range data notification config failed."); 617 return UWB_RECONFIGURATION_FAILURE; 618 } 619 return STATUS_OK; 620 } 621 622 /** Notifies that a ranging round failed. We collect this info for Analytics only. */ 623 public interface RangingRoundFailureCallback { 624 /** Reports ranging round failed. */ 625 void onRangingRoundFailed(UwbAddress peerAddress); 626 } 627 628 /** Sets RangingRoundFailureCallback. */ 629 public void setRangingRoundFailureCallback( 630 @Nullable RangingRoundFailureCallback rangingRoundFailureCallback) { 631 this.mRangingRoundFailureCallback = rangingRoundFailureCallback; 632 } 633 634 /** Sets the system callback executor. */ 635 public void setSystemCallbackExecutor(Executor executor) { 636 this.mSystemCallbackExecutor = executor; 637 } 638 } 639