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 923730cc0590e3b886d2eac8a9c6eb34eeeb81dc..4b5874b84554e3b2b329e87d943589a2f5a8dda9 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 adb36c5a31e9903aa33b185b735e9f49e27d43f8..a922168806c31f889b99d7ba6e092235f022219d 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 @@ import java.util.concurrent.Executor; * <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 2015b666cbc88aba3de4e5a13ff51b724620706f..62463e15106d7ef965f4017bbc12c0356e71984b 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 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 com.android.internal.util.Preconditions; + +import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; -/** UWB (ultra wide-band) implementation of {@link RangingSession}. */ +/** 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 0000000000000000000000000000000000000000..3be5e7078ff11d5aa577be0fc9ab4d51c0ef5fe9 --- /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 0000000000000000000000000000000000000000..91198abca398874ed37384b6b822fe03725acd3d --- /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); + } +}