[remoteauth] Implement UwbRangingSession
Test: atest RemoteAuthUnitTests
Bug: 290675814
Change-Id: I9a38298092607fe87bc7a8b157ff9d44d84fa55a
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
index 923730c..4b5874b 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
@@ -15,5 +15,67 @@
*/
package com.android.server.remoteauth.ranging;
-/** The set of parameters to start ranging. */
-public class RangingParameters {}
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+
+/** The set of parameters to initiate {@link RangingSession#start}. */
+public class RangingParameters {
+
+ /** Parameters for {@link UwbRangingSession}. */
+ private final UwbAddress mUwbLocalAddress;
+
+ private final androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+ public UwbAddress getUwbLocalAddress() {
+ return mUwbLocalAddress;
+ }
+
+ public androidx.core.uwb.backend.impl.internal.RangingParameters getUwbRangingParameters() {
+ return mUwbRangingParameters;
+ }
+
+ private RangingParameters(
+ UwbAddress uwbLocalAddress,
+ androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+ mUwbLocalAddress = uwbLocalAddress;
+ mUwbRangingParameters = uwbRangingParameters;
+ }
+
+ /** Builder class for {@link RangingParameters}. */
+ public static final class Builder {
+ private UwbAddress mUwbLocalAddress;
+ private androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+ /**
+ * Sets the uwb local address.
+ *
+ * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+ * RANGING_METHOD_UWB} and {@link SessionParameters#getAutoDeriveParams} == false
+ */
+ public Builder setUwbLocalAddress(UwbAddress uwbLocalAddress) {
+ mUwbLocalAddress = uwbLocalAddress;
+ return this;
+ }
+
+ /**
+ * Sets the uwb ranging parameters.
+ *
+ * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+ * RANGING_METHOD_UWB}.
+ *
+ * <p>If {@link SessionParameters#getAutoDeriveParams} == true, all required uwb parameters
+ * including uwbLocalAddress, complexChannel, peerAddresses, and sessionKeyInfo will be
+ * automatically derived, so unnecessary to provide and the other uwb parameters are
+ * optional.
+ */
+ public Builder setUwbRangingParameters(
+ androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+ mUwbRangingParameters = uwbRangingParameters;
+ return this;
+ }
+
+ /** Builds {@link RangingParameters}. */
+ public RangingParameters build() {
+ return new RangingParameters(mUwbLocalAddress, mUwbRangingParameters);
+ }
+ }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
index adb36c5..a922168 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
@@ -37,7 +37,8 @@
* <p>A session can be started and stopped multiple times. After starting, updates ({@link
* RangingReport}, {@link RangingError}, etc) will be reported via the provided {@link
* RangingCallback}. BaseKey and SyncData are used for auto derivation of supported ranging
- * parameters, which will be implementation specific.
+ * parameters, which will be implementation specific. All session creation shall only be conducted
+ * via {@link RangingManager#createSession}.
*
* <p>Ranging method specific implementation shall be implemented in the extended class.
*/
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
index 2015b66..62463e1 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
@@ -15,30 +15,219 @@
*/
package com.android.server.remoteauth.ranging;
-import android.annotation.NonNull;
-import android.content.Context;
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+import static androidx.core.uwb.backend.impl.internal.Utils.SUPPORTED_BPRF_PREAMBLE_INDEX;
+import static androidx.core.uwb.backend.impl.internal.UwbAddress.SHORT_ADDRESS_LENGTH;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_OUTSIDE;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingDevice;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback.RangingSuspendedReason;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
-import java.util.concurrent.Executor;
+import com.android.internal.util.Preconditions;
-/** UWB (ultra wide-band) implementation of {@link RangingSession}. */
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** UWB (ultra wide-band) specific implementation of {@link RangingSession}. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class UwbRangingSession extends RangingSession {
- private static final int DERIVED_DATA_LENGTH = 1;
+ private static final String TAG = "UwbRangingSession";
+
+ private static final int COMPLEX_CHANNEL_LENGTH = 1;
+ private static final int STS_KEY_LENGTH = 16;
+ private static final int DERIVED_DATA_LENGTH =
+ COMPLEX_CHANNEL_LENGTH + SHORT_ADDRESS_LENGTH + SHORT_ADDRESS_LENGTH + STS_KEY_LENGTH;
+
+ private final UwbServiceImpl mUwbServiceImpl;
+ private final RangingDevice mRangingDevice;
+
+ private Executor mExecutor;
+ private RangingCallback mRangingCallback;
public UwbRangingSession(
@NonNull Context context,
@NonNull SessionParameters sessionParameters,
@NonNull UwbServiceImpl uwbServiceImpl) {
super(context, sessionParameters, DERIVED_DATA_LENGTH);
+ Preconditions.checkNotNull(uwbServiceImpl);
+ mUwbServiceImpl = uwbServiceImpl;
+ if (sessionParameters.getDeviceRole() == DEVICE_ROLE_INITIATOR) {
+ mRangingDevice = (RangingDevice) mUwbServiceImpl.getController(context);
+ } else {
+ mRangingDevice = (RangingDevice) mUwbServiceImpl.getControlee(context);
+ }
}
@Override
public void start(
@NonNull RangingParameters rangingParameters,
@NonNull Executor executor,
- @NonNull RangingCallback rangingCallback) {}
+ @NonNull RangingCallback rangingCallback) {
+ Preconditions.checkNotNull(rangingParameters, "rangingParameters must not be null");
+ Preconditions.checkNotNull(executor, "executor must not be null");
+ Preconditions.checkNotNull(rangingCallback, "rangingCallback must not be null");
+
+ setUwbRangingParameters(rangingParameters);
+ int status =
+ mRangingDevice.startRanging(
+ convertCallback(rangingCallback, executor),
+ Executors.newSingleThreadExecutor());
+ if (status != STATUS_OK) {
+ Log.w(TAG, String.format("Uwb ranging start failed with status %d", status));
+ executor.execute(
+ () -> rangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_START));
+ return;
+ }
+ mExecutor = executor;
+ mRangingCallback = rangingCallback;
+ Log.i(TAG, "start");
+ }
@Override
- public void stop() {}
+ public void stop() {
+ if (mRangingCallback == null) {
+ Log.w(TAG, String.format("Failed to stop unstarted session"));
+ return;
+ }
+ int status = mRangingDevice.stopRanging();
+ if (status != STATUS_OK) {
+ Log.w(TAG, String.format("Uwb ranging stop failed with status %d", status));
+ mExecutor.execute(
+ () -> mRangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_STOP));
+ return;
+ }
+ mRangingCallback = null;
+ Log.i(TAG, "stop");
+ }
+
+ private void setUwbRangingParameters(RangingParameters rangingParameters) {
+ androidx.core.uwb.backend.impl.internal.RangingParameters params =
+ rangingParameters.getUwbRangingParameters();
+ Preconditions.checkNotNull(params, "uwbRangingParameters must not be null");
+ if (mAutoDeriveParams) {
+ Preconditions.checkArgument(mDerivedData.length == DERIVED_DATA_LENGTH);
+ ByteBuffer buffer = ByteBuffer.wrap(mDerivedData);
+
+ byte complexChannelByte = buffer.get();
+ int preambleIndex =
+ SUPPORTED_BPRF_PREAMBLE_INDEX.get(
+ Math.abs(complexChannelByte) % SUPPORTED_BPRF_PREAMBLE_INDEX.size());
+ // Selecting channel 9 since it's the only mandatory channel.
+ UwbComplexChannel complexChannel = new UwbComplexChannel(UWB_CHANNEL_9, preambleIndex);
+
+ byte[] localAddress = new byte[SHORT_ADDRESS_LENGTH];
+ byte[] peerAddress = new byte[SHORT_ADDRESS_LENGTH];
+ if (mRangingDevice instanceof RangingController) {
+ ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+ buffer.get(localAddress);
+ buffer.get(peerAddress);
+ } else {
+ buffer.get(peerAddress);
+ buffer.get(localAddress);
+ }
+ byte[] stsKey = new byte[STS_KEY_LENGTH];
+ buffer.get(stsKey);
+
+ mRangingDevice.setLocalAddress(UwbAddress.fromBytes(localAddress));
+ mRangingDevice.setRangingParameters(
+ new androidx.core.uwb.backend.impl.internal.RangingParameters(
+ params.getUwbConfigId(),
+ SESSION_ID_UNSET,
+ /* subSessionId= */ SESSION_ID_UNSET,
+ stsKey,
+ /* subSessionInfo= */ new byte[] {},
+ complexChannel,
+ List.of(UwbAddress.fromBytes(peerAddress)),
+ params.getRangingUpdateRate(),
+ params.getUwbRangeDataNtfConfig(),
+ params.getSlotDuration(),
+ params.isAoaDisabled()));
+ } else {
+ UwbAddress localAddress = rangingParameters.getUwbLocalAddress();
+ Preconditions.checkNotNull(localAddress, "localAddress must not be null");
+ UwbComplexChannel complexChannel = params.getComplexChannel();
+ Preconditions.checkNotNull(complexChannel, "complexChannel must not be null");
+ mRangingDevice.setLocalAddress(localAddress);
+ if (mRangingDevice instanceof RangingController) {
+ ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+ }
+ mRangingDevice.setRangingParameters(params);
+ }
+ }
+
+ private RangingSessionCallback convertCallback(RangingCallback callback, Executor executor) {
+ return new RangingSessionCallback() {
+
+ @Override
+ public void onRangingInitialized(UwbDevice device) {
+ Log.i(TAG, "onRangingInitialized");
+ }
+
+ @Override
+ public void onRangingResult(UwbDevice device, RangingPosition position) {
+ float distanceM = position.getDistance().getValue();
+ int proximityState =
+ (mLowerProximityBoundaryM <= distanceM
+ && distanceM <= mUpperProximityBoundaryM)
+ ? PROXIMITY_STATE_INSIDE
+ : PROXIMITY_STATE_OUTSIDE;
+ position.getDistance().getValue();
+ RangingReport rangingReport =
+ new RangingReport.Builder()
+ .setDistanceM(distanceM)
+ .setProximityState(proximityState)
+ .build();
+ executor.execute(() -> callback.onRangingReport(mSessionInfo, rangingReport));
+ }
+
+ @Override
+ public void onRangingSuspended(UwbDevice device, @RangingSuspendedReason int reason) {
+ executor.execute(() -> callback.onError(mSessionInfo, convertError(reason)));
+ }
+ };
+ }
+
+ @RangingError
+ private static int convertError(@RangingSuspendedReason int reason) {
+ if (reason == RangingSessionCallback.REASON_WRONG_PARAMETERS) {
+ return RANGING_ERROR_INVALID_PARAMETERS;
+ }
+ if (reason == RangingSessionCallback.REASON_STOP_RANGING_CALLED) {
+ return RANGING_ERROR_STOPPED_BY_REQUEST;
+ }
+ if (reason == RangingSessionCallback.REASON_STOPPED_BY_PEER) {
+ return RANGING_ERROR_STOPPED_BY_PEER;
+ }
+ if (reason == RangingSessionCallback.REASON_FAILED_TO_START) {
+ return RANGING_ERROR_FAILED_TO_START;
+ }
+ if (reason == RangingSessionCallback.REASON_SYSTEM_POLICY) {
+ return RANGING_ERROR_SYSTEM_ERROR;
+ }
+ if (reason == RangingSessionCallback.REASON_MAX_RANGING_ROUND_RETRY_REACHED) {
+ return RANGING_ERROR_SYSTEM_TIMEOUT;
+ }
+ return RANGING_ERROR_UNKNOWN;
+ }
}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
new file mode 100644
index 0000000..3be5e70
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.remoteauth.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Unit test for {@link RangingParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingParametersTest {
+
+ private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+ UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+ private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+ TEST_UWB_RANGING_PARAMETERS =
+ new androidx.core.uwb.backend.impl.internal.RangingParameters(
+ CONFIG_PROVISIONED_UNICAST_DS_TWR,
+ /* sessionId= */ SESSION_ID_UNSET,
+ /* subSessionId= */ SESSION_ID_UNSET,
+ /* SessionInfo= */ new byte[] {},
+ /* subSessionInfo= */ new byte[] {},
+ new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9),
+ List.of(UwbAddress.fromBytes(new byte[] {0x00, 0x02})),
+ /* rangingUpdateRate= */ NORMAL,
+ new UwbRangeDataNtfConfig.Builder().build(),
+ /* slotDuration= */ DURATION_1_MS,
+ /* isAoaDisabled= */ false);
+
+ @Test
+ public void testBuildingRangingParameters_success() {
+ final RangingParameters rangingParameters =
+ new RangingParameters.Builder()
+ .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+ .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+ .build();
+
+ assertEquals(rangingParameters.getUwbLocalAddress(), TEST_UWB_LOCAL_ADDRESS);
+ assertEquals(rangingParameters.getUwbRangingParameters(), TEST_UWB_RANGING_PARAMETERS);
+ }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
new file mode 100644
index 0000000..91198ab
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.remoteauth.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.RangingMeasurement.CONFIDENCE_HIGH;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_ERROR;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_START;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_STOP;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_RESPONDER;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.core.uwb.backend.impl.internal.RangingControlee;
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingMeasurement;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.RangingSession.RangingCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link UwbRangingSession}. */
+@RunWith(AndroidJUnit4.class)
+public class UwbRangingSessionTest {
+
+ private static final String TEST_DEVICE_ID = "test_device_id";
+ @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+ private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+ private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+ private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+ private static final byte[] TEST_BASE_KEY =
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f
+ };
+ private static final byte[] TEST_SYNC_DATA =
+ new byte[] {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+ 0x0f, 0x00
+ };
+ private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR =
+ new SessionParameters.Builder()
+ .setDeviceId(TEST_DEVICE_ID)
+ .setRangingMethod(TEST_RANGING_METHOD)
+ .setDeviceRole(DEVICE_ROLE_INITIATOR)
+ .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+ .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+ .build();
+ private static final SessionParameters TEST_SESSION_PARAMETER_RESPONDER =
+ new SessionParameters.Builder()
+ .setDeviceId(TEST_DEVICE_ID)
+ .setRangingMethod(TEST_RANGING_METHOD)
+ .setDeviceRole(DEVICE_ROLE_RESPONDER)
+ .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+ .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+ .build();
+ private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR_W_AD =
+ new SessionParameters.Builder()
+ .setDeviceId(TEST_DEVICE_ID)
+ .setRangingMethod(TEST_RANGING_METHOD)
+ .setDeviceRole(DEVICE_ROLE_INITIATOR)
+ .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+ .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+ .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+ .setBaseKey(TEST_BASE_KEY)
+ .setSyncData(TEST_SYNC_DATA)
+ .build();
+ private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+ UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+ private static final UwbAddress TEST_UWB_PEER_ADDRESS =
+ UwbAddress.fromBytes(new byte[] {0x00, 0x02});
+ private static final UwbComplexChannel TEST_UWB_COMPLEX_CHANNEL =
+ new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9);
+ private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+ TEST_UWB_RANGING_PARAMETERS =
+ new androidx.core.uwb.backend.impl.internal.RangingParameters(
+ CONFIG_PROVISIONED_UNICAST_DS_TWR,
+ /* sessionId= */ SESSION_ID_UNSET,
+ /* subSessionId= */ SESSION_ID_UNSET,
+ /* SessionInfo= */ new byte[] {},
+ /* subSessionInfo= */ new byte[] {},
+ TEST_UWB_COMPLEX_CHANNEL,
+ List.of(TEST_UWB_PEER_ADDRESS),
+ NORMAL,
+ new UwbRangeDataNtfConfig.Builder().build(),
+ DURATION_1_MS,
+ /* isAoaDisabled= */ false);
+ private static final RangingParameters TEST_RANGING_PARAMETERS =
+ new RangingParameters.Builder()
+ .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+ .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+ .build();
+ private static final UwbAddress TEST_DERIVED_UWB_LOCAL_ADDRESS =
+ UwbAddress.fromBytes(new byte[] {0x4C, (byte) 0xB4});
+ private static final UwbAddress TEST_DERIVED_UWB_PEER_ADDRESS =
+ UwbAddress.fromBytes(new byte[] {(byte) 0xAE, 0x2E});
+ private static final UwbComplexChannel TEST_DERIVED_UWB_COMPLEX_CHANNEL =
+ new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 12);
+ private static final byte[] TEST_DERIVED_STS_KEY =
+ new byte[] {
+ 0x76,
+ (byte) 0xD7,
+ (byte) 0xB6,
+ 0x1A,
+ (byte) 0x8D,
+ 0x29,
+ 0x1A,
+ 0x52,
+ (byte) 0xBB,
+ (byte) 0xBF,
+ (byte) 0xE6,
+ 0x28,
+ (byte) 0xAD,
+ 0x44,
+ (byte) 0xFB,
+ 0x2E
+ };
+
+ private static final UwbDevice TEST_UWB_DEVICE =
+ UwbDevice.createForAddress(TEST_UWB_PEER_ADDRESS.toBytes());
+ private static final float TEST_DISTANCE = 1.5f;
+ private static final RangingMeasurement TEST_RANGING_MEASUREMENT =
+ new RangingMeasurement(
+ /* confidence= */ CONFIDENCE_HIGH,
+ /* value= */ TEST_DISTANCE,
+ /* valid= */ true);
+ private static final RangingPosition TEST_RANGING_POSITION =
+ new RangingPosition(
+ TEST_RANGING_MEASUREMENT,
+ /* azimuth= */ null,
+ /* elevation= */ null,
+ /* dlTdoaMeasurement= */ null,
+ /* elapsedRealtimeNanos= */ 0,
+ /* rssi= */ 0);
+
+ @Mock private Context mContext;
+ @Mock private UwbServiceImpl mUwbServiceImpl;
+ @Mock private RangingController mRangingController;
+ @Mock private RangingControlee mRangingControlee;
+ @Mock private RangingCallback mRangingCallback;
+ @Mock private Executor mCallbackExecutor;
+
+ private UwbRangingSession mUwbRangingSession;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mUwbServiceImpl.getController(mContext)).thenReturn(mRangingController);
+ when(mUwbServiceImpl.getControlee(mContext)).thenReturn(mRangingControlee);
+ when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_OK);
+ when(mRangingControlee.startRanging(any(), any())).thenReturn(STATUS_OK);
+ doAnswer(
+ invocation -> {
+ Runnable t = invocation.getArgument(0);
+ t.run();
+ return true;
+ })
+ .when(mCallbackExecutor)
+ .execute(any(Runnable.class));
+ }
+
+ @Test
+ public void testConstruction_nullArgument() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new UwbRangingSession(
+ null, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl));
+ assertThrows(
+ NullPointerException.class,
+ () -> new UwbRangingSession(mContext, null, mUwbServiceImpl));
+ assertThrows(
+ NullPointerException.class,
+ () -> new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, null));
+ }
+
+ @Test
+ public void testConstruction_initiatorSuccess() {
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+ verify(mUwbServiceImpl, times(1)).getController(mContext);
+ }
+
+ @Test
+ public void testConstruction_responderSuccess() {
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_RESPONDER, mUwbServiceImpl);
+ verify(mUwbServiceImpl, times(1)).getControlee(mContext);
+ }
+
+ @Test
+ public void testStart_nullArgument() {
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+
+ assertThrows(
+ NullPointerException.class,
+ () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, null));
+ assertThrows(
+ NullPointerException.class,
+ () -> mUwbRangingSession.start(null, mCallbackExecutor, mRangingCallback));
+ assertThrows(
+ NullPointerException.class,
+ () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, null, mRangingCallback));
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ mUwbRangingSession.start(
+ new RangingParameters.Builder().build(),
+ mCallbackExecutor,
+ mRangingCallback));
+ }
+
+ @Test
+ public void testStart_initiatorWithoutADFailed() {
+ when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_ERROR);
+
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+ mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+ verify(mRangingController, times(1)).setComplexChannel(TEST_UWB_COMPLEX_CHANNEL);
+ verify(mRangingController, times(1)).setLocalAddress(TEST_UWB_LOCAL_ADDRESS);
+ verify(mRangingController, times(1)).setRangingParameters(TEST_UWB_RANGING_PARAMETERS);
+ verify(mRangingController, times(1)).startRanging(any(), any());
+ ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+ verify(mRangingCallback, times(1))
+ .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_START));
+ assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+ }
+
+ private void testRangingCallback() {
+ Answer startRangingResponse =
+ new Answer() {
+ public Object answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ RangingSessionCallback cb = (RangingSessionCallback) args[0];
+ cb.onRangingInitialized(TEST_UWB_DEVICE);
+ cb.onRangingResult(TEST_UWB_DEVICE, TEST_RANGING_POSITION);
+ return STATUS_OK;
+ }
+ };
+ doAnswer(startRangingResponse)
+ .when(mRangingController)
+ .startRanging(any(RangingSessionCallback.class), any());
+ }
+
+ @Test
+ public void testStart_initiatorWithADSucceed() {
+ testRangingCallback();
+ mUwbRangingSession =
+ new UwbRangingSession(
+ mContext, TEST_SESSION_PARAMETER_INITIATOR_W_AD, mUwbServiceImpl);
+ mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+ verify(mRangingController, times(1)).setComplexChannel(TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+ verify(mRangingController, times(1)).setLocalAddress(TEST_DERIVED_UWB_LOCAL_ADDRESS);
+ ArgumentCaptor<androidx.core.uwb.backend.impl.internal.RangingParameters> captor =
+ ArgumentCaptor.forClass(
+ androidx.core.uwb.backend.impl.internal.RangingParameters.class);
+ verify(mRangingController, times(1)).setRangingParameters(captor.capture());
+ assertEquals(
+ captor.getValue().getUwbConfigId(), TEST_UWB_RANGING_PARAMETERS.getUwbConfigId());
+ assertEquals(captor.getValue().getSessionId(), SESSION_ID_UNSET);
+ assertEquals(captor.getValue().getSubSessionId(), SESSION_ID_UNSET);
+ assertArrayEquals(captor.getValue().getSessionKeyInfo(), TEST_DERIVED_STS_KEY);
+ assertArrayEquals(captor.getValue().getSubSessionKeyInfo(), new byte[] {});
+ assertEquals(captor.getValue().getComplexChannel(), TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+ assertEquals(captor.getValue().getPeerAddresses().get(0), TEST_DERIVED_UWB_PEER_ADDRESS);
+ assertEquals(
+ captor.getValue().getRangingUpdateRate(),
+ TEST_UWB_RANGING_PARAMETERS.getRangingUpdateRate());
+ assertEquals(
+ captor.getValue().getUwbRangeDataNtfConfig(),
+ TEST_UWB_RANGING_PARAMETERS.getUwbRangeDataNtfConfig());
+ assertEquals(
+ captor.getValue().getSlotDuration(), TEST_UWB_RANGING_PARAMETERS.getSlotDuration());
+ assertEquals(
+ captor.getValue().isAoaDisabled(), TEST_UWB_RANGING_PARAMETERS.isAoaDisabled());
+ verify(mRangingController, times(1)).startRanging(any(), any());
+ ArgumentCaptor<SessionInfo> captor2 = ArgumentCaptor.forClass(SessionInfo.class);
+ ArgumentCaptor<RangingReport> captor3 = ArgumentCaptor.forClass(RangingReport.class);
+ verify(mRangingCallback, times(1)).onRangingReport(captor2.capture(), captor3.capture());
+ assertEquals(captor2.getValue().getDeviceId(), TEST_DEVICE_ID);
+ RangingReport rangingReport = captor3.getValue();
+ assertEquals(rangingReport.getDistanceM(), TEST_DISTANCE, 0.0f);
+ assertEquals(rangingReport.getProximityState(), PROXIMITY_STATE_INSIDE);
+ }
+
+ @Test
+ public void testStop_sessionNotStarted() {
+ when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+ mUwbRangingSession.stop();
+
+ verifyZeroInteractions(mRangingController);
+ verifyZeroInteractions(mRangingCallback);
+ }
+
+ @Test
+ public void testStop_failed() {
+ when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+ mUwbRangingSession =
+ new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+ mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+ mUwbRangingSession.stop();
+
+ verify(mRangingController, times(1)).setComplexChannel(any());
+ verify(mRangingController, times(1)).setLocalAddress(any());
+ verify(mRangingController, times(1)).setRangingParameters(any());
+ verify(mRangingController, times(1)).startRanging(any(), any());
+ verify(mRangingController, times(1)).stopRanging();
+ ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+ verify(mRangingCallback, times(1))
+ .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_STOP));
+ assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+ }
+}