summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--OWNERS4
-rw-r--r--service/Android.bp2
-rw-r--r--service/java/com/android/server/uwb/UwbInjector.java7
-rw-r--r--service/java/com/android/server/uwb/UwbServiceCore.java53
-rw-r--r--service/java/com/android/server/uwb/UwbSessionManager.java106
-rw-r--r--service/java/com/android/server/uwb/config/CapabilityParam.java2
-rw-r--r--service/java/com/android/server/uwb/discovery/TransportServerProvider.java88
-rw-r--r--service/java/com/android/server/uwb/discovery/TransportServerService.java81
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java6
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java6
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java10
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java494
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/UuidConstants.java46
-rw-r--r--service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java2
-rw-r--r--service/java/com/android/server/uwb/params/FiraDecoder.java11
-rw-r--r--service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java15
-rw-r--r--service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java52
-rw-r--r--service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java24
-rw-r--r--service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java111
-rw-r--r--service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java150
-rw-r--r--service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java122
-rw-r--r--service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java522
-rw-r--r--service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java5
23 files changed, 1713 insertions, 206 deletions
diff --git a/OWNERS b/OWNERS
index 07256436..082f4eac 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,7 @@
rpius@google.com
steveliu@google.com
zning@google.com
+
+# For the rust codebase
+per-file *.rs = akahuang@google.com
+per-file *.bp = akahuang@google.com \ No newline at end of file
diff --git a/service/Android.bp b/service/Android.bp
index 3b154b6c..bcc3301c 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -64,7 +64,7 @@ java_library {
static_libs: [
"androidx.annotation_annotation",
- "android.hardware.uwb.fira_android-V1-java",
+ "android.hardware.uwb.fira_android-V2-java",
"com.uwb.support.ccc",
"com.uwb.support.fira",
"com.uwb.support.generic",
diff --git a/service/java/com/android/server/uwb/UwbInjector.java b/service/java/com/android/server/uwb/UwbInjector.java
index c99c3ce2..11453a78 100644
--- a/service/java/com/android/server/uwb/UwbInjector.java
+++ b/service/java/com/android/server/uwb/UwbInjector.java
@@ -32,6 +32,7 @@ import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -330,6 +331,12 @@ public class UwbInjector {
return false;
}
+ /** Whether the uid is signed with the same key as the platform. */
+ public boolean isAppSignedWithPlatformKey(int uid) {
+ return mContext.getPackageManager().checkSignatures(uid, Process.SYSTEM_UID)
+ == PackageManager.SIGNATURE_MATCH;
+ }
+
/** Helper method to retrieve app importance. */
private int getPackageImportance(int uid, @NonNull String packageName) {
try {
diff --git a/service/java/com/android/server/uwb/UwbServiceCore.java b/service/java/com/android/server/uwb/UwbServiceCore.java
index 667eb94e..aa94185e 100644
--- a/service/java/com/android/server/uwb/UwbServiceCore.java
+++ b/service/java/com/android/server/uwb/UwbServiceCore.java
@@ -39,9 +39,9 @@ import android.uwb.SessionHandle;
import android.uwb.StateChangeReason;
import android.uwb.UwbManager.AdapterStateCallback;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.uwb.data.UwbUciConstants;
import com.android.server.uwb.data.UwbVendorUciResponse;
import com.android.server.uwb.jni.INativeUwbManager;
@@ -76,8 +76,12 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
INativeUwbManager.VendorNotification, UwbCountryCode.CountryCodeChangedListener {
private static final String TAG = "UwbServiceCore";
- private static final int TASK_ENABLE = 1;
- private static final int TASK_DISABLE = 2;
+ @VisibleForTesting
+ public static final int TASK_ENABLE = 1;
+ @VisibleForTesting
+ public static final int TASK_DISABLE = 2;
+ @VisibleForTesting
+ public static final int TASK_RESTART = 3;
private static final int WATCHDOG_MS = 10000;
private static final int SEND_VENDOR_CMD_TIMEOUT_MS = 10000;
@@ -181,10 +185,10 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
// If error status is received, toggle UWB off to reset stack state.
// TODO(b/227488208): Should we try to restart (like wifi) instead?
if ((byte) deviceState == UwbUciConstants.DEVICE_STATE_ERROR) {
- Log.e(TAG, "Error device status received. Disabling...");
+ Log.e(TAG, "Error device status received. Restarting...");
mUwbMetrics.incrementDeviceStatusErrorCount();
- takBugReportAfterDeviceError("UWB is disabled due to device status error");
- setEnabled(false);
+ takBugReportAfterDeviceError("Restarting UWB due to vendor error");
+ mEnableDisableTask.execute(TASK_RESTART);
return;
}
handleDeviceStatusNotification(deviceState);
@@ -267,30 +271,6 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
return mNativeUwbManager.getTimestampResolutionNanos();
}
- /**
- * Check the attribution source chain to ensure that there are no 3p apps which are not in fg
- * which can receive the ranging results.
- * @return true if there is some non-system app which is in not in fg, false otherwise.
- */
- private boolean hasAnyNonSystemAppNotInFgInAttributionSource(
- @NonNull AttributionSource attributionSource) {
- // Iterate attribution source chain to ensure that there is no non-fg 3p app in the
- // request.
- while (attributionSource != null) {
- int uid = attributionSource.getUid();
- String packageName = attributionSource.getPackageName();
- if (!mUwbInjector.isSystemApp(uid, packageName)) {
- if (!mUwbInjector.isForegroundAppOrService(uid, packageName)) {
- Log.e(TAG, "Found a non fg app/service in the attribution source of request: "
- + attributionSource);
- return true;
- }
- }
- attributionSource = attributionSource.getNext();
- }
- return false;
- }
-
public void openRanging(
AttributionSource attributionSource,
SessionHandle sessionHandle,
@@ -299,12 +279,6 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
if (!isUwbEnabled()) {
throw new IllegalStateException("Uwb is not enabled");
}
- if (hasAnyNonSystemAppNotInFgInAttributionSource(attributionSource)) {
- Log.e(TAG, "openRanging - System policy disallows");
- rangingCallbacks.onRangingOpenFailed(sessionHandle,
- RangingChangeReason.SYSTEM_POLICY, new PersistableBundle());
- return;
- }
int sessionId = 0;
if (FiraParams.isCorrectProtocol(params)) {
FiraOpenSessionParams firaOpenSessionParams = FiraOpenSessionParams.fromBundle(
@@ -479,6 +453,13 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
mSessionManager.deinitAllSession();
disableInternal();
break;
+
+ case TASK_RESTART:
+ mSessionManager.deinitAllSession();
+ disableInternal();
+ enableInternal();
+ break;
+
default:
Log.d(TAG, "EnableDisableTask : Undefined Task");
break;
diff --git a/service/java/com/android/server/uwb/UwbSessionManager.java b/service/java/com/android/server/uwb/UwbSessionManager.java
index 76bae008..bd05f9f8 100644
--- a/service/java/com/android/server/uwb/UwbSessionManager.java
+++ b/service/java/com/android/server/uwb/UwbSessionManager.java
@@ -28,6 +28,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.PersistableBundle;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
@@ -53,6 +54,7 @@ import com.google.uwb.support.ccc.CccOpenRangingParams;
import com.google.uwb.support.ccc.CccParams;
import com.google.uwb.support.ccc.CccRangingStartedParams;
import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
import com.google.uwb.support.fira.FiraParams;
import com.google.uwb.support.fira.FiraRangingReconfigureParams;
@@ -73,12 +75,18 @@ import java.util.concurrent.TimeoutException;
public class UwbSessionManager implements INativeUwbManager.SessionNotification {
private static final String TAG = "UwbSessionManager";
- private static final int SESSION_OPEN_RANGING = 1;
- private static final int SESSION_START_RANGING = 2;
- private static final int SESSION_STOP_RANGING = 3;
- private static final int SESSION_RECONFIG_RANGING = 4;
- private static final int SESSION_CLOSE = 5;
- private static final int SESSION_ON_DEINIT = 6;
+ @VisibleForTesting
+ public static final int SESSION_OPEN_RANGING = 1;
+ @VisibleForTesting
+ public static final int SESSION_START_RANGING = 2;
+ @VisibleForTesting
+ public static final int SESSION_STOP_RANGING = 3;
+ @VisibleForTesting
+ public static final int SESSION_RECONFIG_RANGING = 4;
+ @VisibleForTesting
+ public static final int SESSION_CLOSE = 5;
+ @VisibleForTesting
+ public static final int SESSION_ON_DEINIT = 6;
// TODO: don't expose the internal field for testing.
@VisibleForTesting
@@ -211,6 +219,19 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification
Log.i(TAG, "initSession() : Enter - sessionId : " + sessionId);
UwbSession uwbSession = createUwbSession(attributionSource, sessionHandle, sessionId,
protocolName, params, rangingCallbacks);
+ // Check the attribution source chain to ensure that there are no 3p apps which are not in
+ // fg which can receive the ranging results.
+ AttributionSource nonPrivilegedAppAttrSource =
+ uwbSession.hasAnyNonPrivilegedAppInAttributionSource();
+ if (nonPrivilegedAppAttrSource != null && !mUwbInjector.isForegroundAppOrService(
+ nonPrivilegedAppAttrSource.getUid(), nonPrivilegedAppAttrSource.getPackageName())) {
+ Log.e(TAG, "Found a non fg 3p app/service in the attribution source of request: "
+ + nonPrivilegedAppAttrSource);
+ Log.e(TAG, "openRanging - System policy disallows for non fg 3p apps");
+ rangingCallbacks.onRangingOpenFailed(sessionHandle,
+ RangingChangeReason.SYSTEM_POLICY, new PersistableBundle());
+ return;
+ }
if (isExistedSession(sessionId)) {
Log.i(TAG, "Duplicated sessionId");
rangingCallbacks.onRangingOpenFailed(sessionHandle, RangingChangeReason.BAD_PARAMETERS,
@@ -430,12 +451,22 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification
}
public int reconfigure(SessionHandle sessionHandle, @Nullable Params params) {
- Log.i(TAG, "reconfigure() - Session Handle : " + sessionHandle);
int status = UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST;
if (!isExistedSession(sessionHandle)) {
Log.i(TAG, "Not initialized session ID");
return status;
}
+ int sessionId = getSessionId(sessionHandle);
+ Log.i(TAG, "reconfigure() - Session ID : " + sessionId);
+ UwbSession uwbSession = getUwbSession(sessionId);
+ if (uwbSession.getProtocolName().equals(FiraParams.PROTOCOL_NAME)
+ && params instanceof FiraRangingReconfigureParams) {
+ FiraRangingReconfigureParams rangingReconfigureParams =
+ (FiraRangingReconfigureParams) params;
+ Log.i(TAG, "reconfigure() - update reconfigure params: "
+ + rangingReconfigureParams);
+ uwbSession.updateFiraParamsOnReconfigure(rangingReconfigureParams);
+ }
Pair<SessionHandle, Params> info = new Pair<>(sessionHandle, params);
mEventTask.execute(SESSION_RECONFIG_RANGING, info);
return 0;
@@ -892,6 +923,31 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification
this.mProfileType = convertProtolNameToProfileType(protocolName);
}
+ private boolean isPrivilegedApp(int uid, String packageName) {
+ return mUwbInjector.isSystemApp(uid, packageName)
+ || mUwbInjector.isAppSignedWithPlatformKey(uid);
+ }
+
+ /**
+ * Check the attribution source chain to check if there are any 3p apps.
+ * @return true if there is some non-system app, false otherwise.
+ */
+ @Nullable
+ public AttributionSource hasAnyNonPrivilegedAppInAttributionSource() {
+ // Iterate attribution source chain to ensure that there is no non-fg 3p app in the
+ // request.
+ AttributionSource attributionSource = mAttributionSource;
+ while (attributionSource != null) {
+ int uid = attributionSource.getUid();
+ String packageName = attributionSource.getPackageName();
+ if (!isPrivilegedApp(uid, packageName)) {
+ return attributionSource;
+ }
+ attributionSource = attributionSource.getNext();
+ }
+ return null;
+ }
+
public AttributionSource getAttributionSource() {
return this.mAttributionSource;
}
@@ -910,25 +966,35 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification
public void updateCccParamsOnStart(CccStartRangingParams rangingStartParams) {
// Need to update the RAN multiplier from the CccStartRangingParams for CCC session.
- CccOpenRangingParams rangingOpenedParams = (CccOpenRangingParams) mParams;
CccOpenRangingParams newParams =
- new CccOpenRangingParams.Builder()
- .setProtocolVersion(rangingOpenedParams.getProtocolVersion())
- .setUwbConfig(rangingOpenedParams.getUwbConfig())
- .setPulseShapeCombo(rangingOpenedParams.getPulseShapeCombo())
- .setSessionId(rangingOpenedParams.getSessionId())
+ new CccOpenRangingParams.Builder((CccOpenRangingParams) mParams)
.setRanMultiplier(rangingStartParams.getRanMultiplier())
- .setChannel(rangingOpenedParams.getChannel())
- .setNumChapsPerSlot(rangingOpenedParams.getNumChapsPerSlot())
- .setNumResponderNodes(rangingOpenedParams.getNumResponderNodes())
- .setNumSlotsPerRound(rangingOpenedParams.getNumSlotsPerRound())
- .setSyncCodeIndex(rangingOpenedParams.getSyncCodeIndex())
- .setHoppingConfigMode(rangingOpenedParams.getHoppingConfigMode())
- .setHoppingSequence(rangingOpenedParams.getHoppingSequence())
.build();
this.mParams = newParams;
}
+ public void updateFiraParamsOnReconfigure(FiraRangingReconfigureParams reconfigureParams) {
+ // Need to update the reconfigure params from the FiraRangingReconfigureParams for
+ // FiRa session.
+ FiraOpenSessionParams.Builder newParamsBuilder =
+ new FiraOpenSessionParams.Builder((FiraOpenSessionParams) mParams);
+ if (reconfigureParams.getBlockStrideLength() != null) {
+ newParamsBuilder.setBlockStrideLength(reconfigureParams.getBlockStrideLength());
+ }
+ if (reconfigureParams.getRangeDataNtfConfig() != null) {
+ newParamsBuilder.setRangeDataNtfConfig(reconfigureParams.getRangeDataNtfConfig());
+ }
+ if (reconfigureParams.getRangeDataProximityNear() != null) {
+ newParamsBuilder.setRangeDataNtfProximityNear(
+ reconfigureParams.getRangeDataProximityNear());
+ }
+ if (reconfigureParams.getRangeDataProximityFar() != null) {
+ newParamsBuilder.setRangeDataNtfProximityFar(
+ reconfigureParams.getRangeDataProximityFar());
+ }
+ this.mParams = newParamsBuilder.build();
+ }
+
public String getProtocolName() {
return this.mProtocolName;
}
diff --git a/service/java/com/android/server/uwb/config/CapabilityParam.java b/service/java/com/android/server/uwb/config/CapabilityParam.java
index a86ee9a4..f7d5a20e 100644
--- a/service/java/com/android/server/uwb/config/CapabilityParam.java
+++ b/service/java/com/android/server/uwb/config/CapabilityParam.java
@@ -43,6 +43,8 @@ public class CapabilityParam {
public static final int SUPPORTED_EXTENDED_MAC_ADDRESS = 0x11;
public static final int SUPPORTED_AOA_RESULT_REQ_INTERLEAVING =
UwbVendorCapabilityTlvTypes.SUPPORTED_AOA_RESULT_REQ_ANTENNA_INTERLEAVING;
+ public static final int SUPPORTED_MIN_RANGING_INTERVAL_MS =
+ UwbVendorCapabilityTlvTypes.SUPPORTED_MIN_RANGING_INTERVAL_MS;
// CCC specific
public static final int CCC_SUPPORTED_VERSIONS =
diff --git a/service/java/com/android/server/uwb/discovery/TransportServerProvider.java b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java
new file mode 100644
index 00000000..2c071167
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage;
+
+/** Abstract class for Transport Server Provider */
+@WorkerThread
+public abstract class TransportServerProvider {
+
+ /** Callback for listening to transport server events. */
+ @WorkerThread
+ public interface TransportServerCallback {
+
+ /** Called when the server started processing. */
+ void onProcessingStarted();
+
+ /** Called when the server stopped processing. */
+ void onProcessingStopped();
+
+ /**
+ * Called when the server receive new capabilites from the remote device.
+ *
+ * @param capabilities new capabilities.
+ */
+ void onCapabilitesUpdated(FiraConnectorCapabilities capabilities);
+
+ /**
+ * Called when the server receive a new FiRa connector message from the remote device.
+ *
+ * @param secid destination SECID on this device.
+ * @param message FiRa connector message.
+ */
+ void onMessage(int secid, FiraConnectorMessage message);
+ }
+
+ /* Indicates whether the server has started.
+ */
+ protected boolean mStarted = false;
+
+ /**
+ * Checks if the server has started.
+ *
+ * @return indicates if the server has started.
+ */
+ public boolean isStarted() {
+ return mStarted;
+ }
+
+ /**
+ * Starts the transport server.
+ *
+ * @return indicates if succeefully started.
+ */
+ public abstract boolean start();
+
+ /**
+ * Stops the transport server.
+ *
+ * @return indicates if succeefully stopped.
+ */
+ public abstract boolean stop();
+
+ /**
+ * Send a FiRa connector message to the remote device through the transport server.
+ *
+ * @param secid destination SECID on remote device.
+ * @param message message to be send.
+ * @return indicates if succeefully started.
+ */
+ public abstract boolean sendMessage(int secid, FiraConnectorMessage message);
+}
diff --git a/service/java/com/android/server/uwb/discovery/TransportServerService.java b/service/java/com/android/server/uwb/discovery/TransportServerService.java
new file mode 100644
index 00000000..8f8093ab
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/TransportServerService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery;
+
+import android.content.AttributionSource;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
+import com.android.server.uwb.discovery.ble.GattTransportServerProvider;
+import com.android.server.uwb.discovery.info.DiscoveryInfo;
+
+/** This service manages the TransportServerProvider. */
+@WorkerThread
+public class TransportServerService {
+ private static final String TAG = TransportServerService.class.getSimpleName();
+
+ private final TransportServerProvider mTransportServerProvider;
+
+ public TransportServerService(
+ AttributionSource attributionSource,
+ Context context,
+ DiscoveryInfo discoveryInfo,
+ TransportServerCallback transportServerCallback)
+ throws AssertionError {
+
+ switch (discoveryInfo.transportType) {
+ case BLE:
+ mTransportServerProvider =
+ new GattTransportServerProvider(
+ attributionSource, context, transportServerCallback);
+ break;
+ default:
+ throw new AssertionError(
+ "Failed to create TransportServerProvider due to invalid transport type:"
+ + " "
+ + discoveryInfo.transportType);
+ }
+ }
+
+ /**
+ * Start the transport server
+ *
+ * @return indicates if succeefully started.
+ */
+ public boolean start() {
+ if (mTransportServerProvider.isStarted()) {
+ Log.i(TAG, "Transport server already started.");
+ return false;
+ }
+ return mTransportServerProvider.start();
+ }
+
+ /**
+ * Stop the transport server
+ *
+ * @return indicates if succeefully stopped.
+ */
+ public boolean stop() {
+ if (!mTransportServerProvider.isStarted()) {
+ Log.i(TAG, "Transport server already stopped.");
+ return false;
+ }
+ return mTransportServerProvider.stop();
+ }
+}
diff --git a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java
index 092b7d32..f70f04c1 100644
--- a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java
+++ b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java
@@ -170,7 +170,7 @@ public class BleDiscoveryAdvertiseProvider extends DiscoveryAdvertiseProvider {
return new AdvertiseData.Builder()
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
- .addServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID)
+ .addServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID)
.build();
}
@@ -179,9 +179,9 @@ public class BleDiscoveryAdvertiseProvider extends DiscoveryAdvertiseProvider {
new AdvertiseData.Builder()
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
- .addServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID)
+ .addServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID)
.addServiceData(
- DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID,
+ UuidConstants.FIRA_CP_PARCEL_UUID,
DiscoveryAdvertisement.toBytes(
mAdvertiseInfo.discoveryAdvertisement,
/*includeVendorSpecificData=*/ false));
diff --git a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java
index f3591fc4..f7758a2d 100644
--- a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java
+++ b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java
@@ -140,7 +140,7 @@ public class BleDiscoveryScanProvider extends DiscoveryScanProvider {
return;
}
- byte[] serviceData = record.getServiceData(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID);
+ byte[] serviceData = record.getServiceData(UuidConstants.FIRA_CP_PARCEL_UUID);
if (serviceData == null) {
Log.w(TAG, "Ignoring scan result. Empty ServiceData");
return;
@@ -183,9 +183,7 @@ public class BleDiscoveryScanProvider extends DiscoveryScanProvider {
}
// Add scan filter for FiRa Connector Primary Service UUID.
scanFilterList.add(
- new ScanFilter.Builder()
- .setServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID)
- .build());
+ new ScanFilter.Builder().setServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID).build());
return scanFilterList;
}
diff --git a/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java
index a832666d..ce848f0b 100644
--- a/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java
+++ b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java
@@ -17,8 +17,6 @@ package com.android.server.uwb.discovery.ble;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.bluetooth.BluetoothUuid;
-import android.os.ParcelUuid;
import android.util.Log;
import android.util.SparseArray;
@@ -43,14 +41,6 @@ import java.util.Optional;
public class DiscoveryAdvertisement {
private static final String LOG_TAG = DiscoveryAdvertisement.class.getSimpleName();
- // The FiRa service UUID for connector primary and connector secondary as defined in Bluetooth
- // Specification Supplement v10. Little endian encoding.
- public static final byte[] FIRA_CP_UUID = new byte[] {(byte) 0xF3, (byte) 0xFF};
- public static final byte[] FIRA_CS_UUID = new byte[] {(byte) 0xF4, (byte) 0xFF};
-
- public static final ParcelUuid FIRA_CP_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CP_UUID);
- public static final ParcelUuid FIRA_CS_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CS_UUID);
-
// Mask and value of the FiRa specific field type field within each AD field.
private static final byte FIRA_SPECIFIC_FIELD_TYPE_MASK = (byte) 0xF0;
private static final byte FIRA_SPECIFIC_FIELD_TYPE_UWB_INDICATION_DATA = 0x1;
diff --git a/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java
new file mode 100644
index 00000000..050e3010
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery.ble;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.ContextParams;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.discovery.TransportServerProvider;
+import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
+import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
+import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+/**
+ * Class for UWB transport server provider using Bluetooth GATT.
+ *
+ * <p>The GATT server simply waits for the discovery from client side. It shall also wait for at
+ * least one valid update of FiRa Connector Capabilities characteristic value from the client side.
+ * Until this happens and until the client enables the Handle Value Notification method on the "OUT"
+ * Control Point characteristic (through Client Characteristic Configuration Descriptor), the server
+ * shall ignore all commands sent by Write methods through the "IN" Control Point characteristic.
+ */
+@WorkerThread
+public class GattTransportServerProvider extends TransportServerProvider {
+ private static final String TAG = GattTransportServerProvider.class.getSimpleName();
+
+ private TransportServerCallback mTransportServerCallback;
+ private BluetoothManager mBluetoothManager;
+ private BluetoothGattServer mBluetoothGattServer;
+ private BluetoothDevice mRemoteGattDevice;
+ private FiraConnectorCapabilities mRemoteCapabilities;
+ private boolean mConnected;
+ private boolean mNotificationEnabled;
+
+ private BluetoothGattService mFiraCPService =
+ new BluetoothGattService(
+ UuidConstants.FIRA_CP_PARCEL_UUID.getUuid(),
+ BluetoothGattService.SERVICE_TYPE_PRIMARY);
+
+ private BluetoothGattCharacteristic mInControlPointCharacteristic;
+ private BluetoothGattCharacteristic mOutControlPointCharacteristic;
+ private BluetoothGattCharacteristic mCapabilitiesCharacteristic;
+
+ private BluetoothGattDescriptor mOutControlPointCccdDescriptor;
+
+ /* Queue of Fira Connector Data Packets from the mInControlPointCharacteristic that are
+ * incomplete to be constructed as FiRa Connector Message.
+ */
+ private ArrayDeque<FiraConnectorDataPacket> mIncompleteInDataPacketQueue;
+
+ /* Wraps Fira Connector Message byte array and the associated SECID.
+ */
+ private static class MessagePacket {
+ public final int secid;
+ public ByteBuffer messageBytes;
+
+ MessagePacket(int secid, ByteBuffer messageBytes) {
+ this.secid = secid;
+ this.messageBytes = messageBytes;
+ }
+ }
+
+ /* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the
+ * mOutControlPointCharacteristic.
+ */
+ private ArrayDeque<MessagePacket> mOutMessageQueue;
+
+ /**
+ * GATT server callbacks responsible for servicing read and write calls from the remote device
+ */
+ private BluetoothGattServerCallback mBluetoothGattServerCallback =
+ new BluetoothGattServerCallback() {
+ @Override
+ public void onConnectionStateChange(
+ BluetoothDevice device, int status, int newState) {
+ Log.i(TAG, "onConnectionStateChange state:" + newState + " Device:" + device);
+ if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ mConnected = false;
+ startProcessing(device);
+ } else if (newState == BluetoothProfile.STATE_CONNECTED) {
+ mConnected = true;
+ startProcessing(device);
+ }
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(
+ BluetoothDevice device,
+ int requestId,
+ int offset,
+ BluetoothGattCharacteristic characteristic) {
+ Log.d(TAG, "onCharacteristicReadRequest");
+ if (characteristic.getUuid().equals(mOutControlPointCharacteristic.getUuid())) {
+ Log.d(TAG, "onRead OutControlPointCharacteristic");
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ mOutControlPointCharacteristic.getValue());
+ processOutDataPacket();
+ } else {
+ Log.w(TAG, "onRead unknown " + characteristic.getUuid());
+ }
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(
+ BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ Log.d(
+ TAG,
+ "onCharacteristicWriteRequest uuid:"
+ + characteristic.getUuid()
+ + ", Length: "
+ + value.length);
+ if (characteristic.getUuid().equals(mCapabilitiesCharacteristic.getUuid())) {
+ Log.i(TAG, "onWrite CapabilitiesCharacteristic");
+ mRemoteCapabilities = FiraConnectorCapabilities.fromBytes(value);
+
+ if (mRemoteCapabilities != null) {
+ mTransportServerCallback.onCapabilitesUpdated(mRemoteCapabilities);
+ startProcessing(device);
+
+ if (responseNeeded) {
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value);
+ }
+ return;
+ }
+ if (responseNeeded) {
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_FAILURE,
+ offset,
+ /*value=*/ null);
+ }
+ } else if (characteristic
+ .getUuid()
+ .equals(mInControlPointCharacteristic.getUuid())) {
+ Log.d(TAG, "onWrite InControlPointCharacteristic");
+
+ boolean success = processInDataPacket(value);
+
+ if (responseNeeded) {
+ if (success) {
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value);
+ } else {
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_FAILURE,
+ offset,
+ /*value=*/ null);
+ }
+ }
+ } else {
+ Log.w(TAG, "onWrite unknown " + characteristic.getUuid());
+ }
+ }
+
+ @Override
+ public void onDescriptorWriteRequest(
+ BluetoothDevice device,
+ int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ Log.d(
+ TAG,
+ "onDescriptorWriteRequest uuid:"
+ + descriptor.getUuid()
+ + ", Length: "
+ + value.length);
+ if (descriptor.getUuid().equals(mOutControlPointCccdDescriptor.getUuid())) {
+ if (Arrays.equals(
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
+ Log.d(TAG, "Enable OutControlPoint value notifications: " + device);
+ mNotificationEnabled = true;
+ startProcessing(device);
+ } else if (Arrays.equals(
+ BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
+ Log.d(TAG, "Disable OutControlPoint value notifications: " + device);
+ mNotificationEnabled = false;
+ startProcessing(device);
+ }
+ if (responseNeeded) {
+ mBluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
+ }
+ } else {
+ Log.w(TAG, "onDescriptorWrite unknown " + descriptor.getUuid());
+ if (responseNeeded) {
+ mBluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_FAILURE,
+ offset,
+ /*value=*/ null);
+ }
+ }
+ }
+ };
+
+ public GattTransportServerProvider(
+ AttributionSource attributionSource,
+ Context context,
+ TransportServerCallback transportServerCallback) {
+ Context attributedContext =
+ context.createContext(
+ new ContextParams.Builder()
+ .setNextAttributionSource(attributionSource)
+ .build());
+ mTransportServerCallback = transportServerCallback;
+ mBluetoothManager = attributedContext.getSystemService(BluetoothManager.class);
+ mBluetoothGattServer =
+ mBluetoothManager.openGattServer(attributedContext, mBluetoothGattServerCallback);
+
+ mIncompleteInDataPacketQueue = new ArrayDeque();
+ mOutMessageQueue = new ArrayDeque();
+
+ setupGattCharacteristic();
+ }
+
+ @Override
+ public boolean start() {
+ if (mBluetoothGattServer == null) {
+ Log.w(TAG, "start failed due to mBluetoothGattServer is null.");
+ return false;
+ }
+ boolean succeed = mBluetoothGattServer.addService(mFiraCPService);
+
+ mStarted = succeed;
+ return succeed;
+ }
+
+ @Override
+ public boolean stop() {
+ if (mBluetoothGattServer == null) {
+ Log.w(TAG, "stop failed due to mBluetoothGattServer is null.");
+ return false;
+ }
+ boolean succeed = mBluetoothGattServer.removeService(mFiraCPService);
+
+ // Clear in/out message queue.
+ mIncompleteInDataPacketQueue.clear();
+ mOutMessageQueue.clear();
+
+ mStarted = !succeed;
+ return succeed;
+ }
+
+ @Override
+ public boolean sendMessage(int secid, FiraConnectorMessage message) {
+ if (!isProcessing()) {
+ Log.w(TAG, "Sent request failed due to server not ready for processing.");
+ return false;
+ }
+ byte[] messageBytes = message.toBytes();
+ if (messageBytes.length > mRemoteCapabilities.maxMessageBufferSize) {
+ Log.w(
+ TAG,
+ "Sent request failed due to message size exceeded remote device capabilities.");
+ return false;
+ }
+ mOutMessageQueue.add(new MessagePacket(secid, ByteBuffer.wrap(messageBytes)));
+
+ // No existing meesage in progress, send this message immediately.
+ if (mOutMessageQueue.size() == 1) {
+ return processOutDataPacket();
+ }
+ return true;
+ }
+
+ /**
+ * Process the next out control data packet from the queue. Notify remote device if new data
+ * packet is set in the {@link mOutControlPointCharacteristic}.
+ *
+ * @return indicate if next out data packet was process successfully.
+ */
+ private boolean processOutDataPacket() {
+ if (!isProcessing()) {
+ Log.w(TAG, "processOutDataPacket failed due to server not ready for processing.");
+ return false;
+ }
+ if (mOutMessageQueue.isEmpty()) {
+ Log.d(TAG, "processOutDataPacket skipped due to empty queue.");
+ return false;
+ }
+ MessagePacket messagePacket = mOutMessageQueue.peek();
+ ByteBuffer byteBuffer = messagePacket.messageBytes;
+ byte[] nextPayload =
+ new byte
+ [Math.min(
+ byteBuffer.remaining(),
+ mRemoteCapabilities.optimizedDataPacketSize
+ - FiraConnectorDataPacket.HEADER_SIZE)];
+ byteBuffer.get(nextPayload);
+
+ FiraConnectorDataPacket dataPacket =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ !byteBuffer.hasRemaining(),
+ messagePacket.secid,
+ nextPayload);
+
+ if (!byteBuffer.hasRemaining()) {
+ mOutMessageQueue.pop();
+ }
+ if (!mOutControlPointCharacteristic.setValue(dataPacket.toBytes())) {
+ Log.w(
+ TAG,
+ "processOutDataPacket failed due to fail to set"
+ + " mOutControlPointCharacteristic.");
+ return false;
+ }
+ if (!mBluetoothGattServer.notifyCharacteristicChanged(
+ mRemoteGattDevice, mOutControlPointCharacteristic, /*confirm=*/ false)) {
+ Log.w(TAG, "processOutDataPacket failed due to fail to notifyCharacteristicChanged.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Process the next in control data packet. Construct the FiraConnectorMEssage if data is
+ * complete, and notify callback with the constructed message.
+ *
+ * @return indicate if next in data packet was process successfully.
+ */
+ private boolean processInDataPacket(byte[] bytes) {
+ if (!isProcessing()) {
+ Log.w(TAG, "processInDataPacket failed due to server not ready for processing.");
+ return false;
+ }
+ FiraConnectorDataPacket latestDataPacket = FiraConnectorDataPacket.fromBytes(bytes);
+ if (latestDataPacket == null) {
+ Log.w(
+ TAG,
+ "processInDataPacket failed due to latest FiraConnectorDataPacket cannot be"
+ + " constructed from bytes.");
+ return false;
+ }
+ if (!mIncompleteInDataPacketQueue.isEmpty()
+ && latestDataPacket.secid != mIncompleteInDataPacketQueue.peek().secid) {
+ Log.w(
+ TAG,
+ "processInDataPacket failed due to latest FiraConnectorDataPacket's SECID"
+ + " doesn't match previous data packet.");
+ return false;
+ }
+ mIncompleteInDataPacketQueue.add(latestDataPacket);
+ if (!latestDataPacket.lastChainingPacket) {
+ return true;
+ }
+ // All data packets of the message has been received. Constructing the message.
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ for (FiraConnectorDataPacket dataPacket : mIncompleteInDataPacketQueue) {
+ byteStream.write(dataPacket.payload, /*off=*/ 0, dataPacket.payload.length);
+ }
+ mIncompleteInDataPacketQueue.clear();
+
+ FiraConnectorMessage message = FiraConnectorMessage.fromBytes(byteStream.toByteArray());
+ if (message == null) {
+ Log.w(
+ TAG,
+ "processInDataPacket failed due to FiraConnectorMessage cannot be constructed"
+ + " from bytes.");
+ return false;
+ }
+
+ mTransportServerCallback.onMessage(latestDataPacket.secid, message);
+ return true;
+ }
+
+ /**
+ * Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through
+ * the In/Out control point characterstic when all conditions are meet to start the FiRa GATT
+ * server.
+ *
+ * @param device Remote Bluetooth device.
+ */
+ private void startProcessing(BluetoothDevice device) {
+ if (!mConnected || !mNotificationEnabled || mRemoteCapabilities == null) {
+ Log.d(
+ TAG,
+ "Gatt server not fully ready: connected="
+ + mConnected
+ + ", notification enabled="
+ + mNotificationEnabled
+ + ", valid"
+ + " capabilities="
+ + (mRemoteCapabilities = null));
+ boolean stopping = isProcessing();
+ mRemoteGattDevice = null;
+ if (stopping) {
+ mTransportServerCallback.onProcessingStopped();
+ }
+ return;
+ }
+ mRemoteGattDevice = device;
+ mTransportServerCallback.onProcessingStarted();
+ }
+
+ /**
+ * Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through
+ * the In/Out control point characterstic when all conditions are meet to start the FiRa GATT
+ * server.
+ *
+ * @return indicate if server has started processing.
+ */
+ private boolean isProcessing() {
+ return mRemoteGattDevice != null;
+ }
+
+ /**
+ * Initialize all of the GATT characteristics with appropriate default values and the required
+ * configurations.
+ */
+ private void setupGattCharacteristic() {
+ mInControlPointCharacteristic =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+ mFiraCPService.addCharacteristic(mInControlPointCharacteristic);
+
+ mOutControlPointCharacteristic =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_READ
+ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+ BluetoothGattCharacteristic.PERMISSION_READ);
+ mOutControlPointCccdDescriptor =
+ new BluetoothGattDescriptor(
+ UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ);
+ mOutControlPointCharacteristic.addDescriptor(mOutControlPointCccdDescriptor);
+ mFiraCPService.addCharacteristic(mOutControlPointCharacteristic);
+
+ mCapabilitiesCharacteristic =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+ mFiraCPService.addCharacteristic(mCapabilitiesCharacteristic);
+ }
+}
diff --git a/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java b/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java
new file mode 100644
index 00000000..0bbac7a9
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery.ble;
+
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+/** UUIDs of the UWB BLE OOB. */
+public class UuidConstants {
+
+ /* The FiRa service UUID for connector primary and connector secondary as defined in Bluetooth
+ * Specification Supplement v10. Little endian encoding.
+ */
+ public static final byte[] FIRA_CP_UUID = new byte[] {(byte) 0xF3, (byte) 0xFF};
+ public static final byte[] FIRA_CS_UUID = new byte[] {(byte) 0xF4, (byte) 0xFF};
+
+ public static final ParcelUuid FIRA_CP_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CP_UUID);
+ public static final ParcelUuid FIRA_CS_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CS_UUID);
+
+ /* FiRa Connector Primary GATT Characteristic UUIDs according to FiRa BLE OOB v1.0
+ * specification.
+ */
+ public static final ParcelUuid CP_IN_CONTROL_POINT_UUID =
+ ParcelUuid.fromString("00002A00-0000-1000-8000-D09200000001");
+ public static final ParcelUuid CP_OUT_CONTROL_POINT_UUID =
+ ParcelUuid.fromString("00002A01-0000-1000-8000-D09200000001");
+ public static final ParcelUuid CP_FIRA_CONNECTOR_CAPABILITIES_UUID =
+ ParcelUuid.fromString("00002A02-0000-1000-8000-D09200000001");
+
+ /* Client Characteristic Configuration Descriptor UUID defined by Bluetooth specification.
+ */
+ public static final ParcelUuid CCCD_UUID = BluetoothUuid.parseUuidFrom(new byte[] {0x29, 0x02});
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java b/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java
index 8a3b09d6..0c652ac0 100644
--- a/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java
+++ b/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java
@@ -41,6 +41,8 @@ public class FiraConnectorDataPacket {
private static final int SECID_BITMASK = 0x7F;
+ public static final int HEADER_SIZE = 1;
+
/** True if this the last packet in a fragmented session, otherwise it is false. */
public final boolean lastChainingPacket;
diff --git a/service/java/com/android/server/uwb/params/FiraDecoder.java b/service/java/com/android/server/uwb/params/FiraDecoder.java
index 276361c2..b821cffc 100644
--- a/service/java/com/android/server/uwb/params/FiraDecoder.java
+++ b/service/java/com/android/server/uwb/params/FiraDecoder.java
@@ -16,6 +16,8 @@
package com.android.server.uwb.params;
+import android.util.Log;
+
import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_180;
import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_90;
import static com.android.server.uwb.config.CapabilityParam.AOA_ELEVATION;
@@ -59,6 +61,7 @@ import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_EXTENDED_M
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_MAC_VERSION_RANGE;
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_PHY_VERSION_RANGE;
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_HPRF_PARAMETER_SETS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_MIN_RANGING_INTERVAL_MS;
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_MULTI_NODE_MODES;
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RANGING_METHOD;
import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RFRAME_CONFIG;
@@ -88,6 +91,8 @@ import java.util.List;
import java.util.stream.IntStream;
public class FiraDecoder extends TlvDecoder {
+ private static final String TAG = "FiraDecoder";
+
@Override
public <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramType) {
if (FiraSpecificationParams.class.equals(paramType)) {
@@ -108,6 +113,12 @@ public class FiraDecoder extends TlvDecoder {
byte[] macVersions = tlvs.getByteArray(SUPPORTED_FIRA_MAC_VERSION_RANGE);
builder.setMinMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 0));
builder.setMaxMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 2));
+ try {
+ int minRangingInterval = tlvs.getInt(SUPPORTED_MIN_RANGING_INTERVAL_MS);
+ builder.setMinRangingIntervalSupported(minRangingInterval);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "SUPPORTED_MIN_RANGING_INTERVAL_MS not found.");
+ }
byte deviceRolesUci = tlvs.getByte(SUPPORTED_DEVICE_ROLES);
EnumSet<DeviceRoleCapabilityFlag> deviceRoles =
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java
index 9ddf0748..bd6fbcd7 100644
--- a/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java
@@ -238,6 +238,21 @@ public class CccOpenRangingParams extends CccParams {
mHoppingSequence.set(builder.mHoppingSequence.get());
}
+ public Builder(@NonNull CccOpenRangingParams params) {
+ mProtocolVersion.set(params.mProtocolVersion);
+ mUwbConfig.set(params.mUwbConfig);
+ mPulseShapeCombo.set(params.mPulseShapeCombo);
+ mSessionId.set(params.mSessionId);
+ mRanMultiplier.set(params.mRanMultiplier);
+ mChannel.set(params.mChannel);
+ mNumChapsPerSlot.set(params.mNumChapsPerSlot);
+ mNumResponderNodes.set(params.mNumResponderNodes);
+ mNumSlotsPerRound.set(params.mNumSlotsPerRound);
+ mSyncCodeIndex.set(params.mSyncCodeIndex);
+ mHoppingConfigMode.set(params.mHoppingConfigMode);
+ mHoppingSequence.set(params.mHoppingSequence);
+ }
+
public Builder setProtocolVersion(CccProtocolVersion version) {
mProtocolVersion.set(version);
return this;
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java
index d462e312..b6a830b3 100644
--- a/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java
@@ -926,6 +926,58 @@ public class FiraOpenSessionParams extends FiraParams {
mAoaType = builder.mAoaType;
}
+ public Builder(@NonNull FiraOpenSessionParams params) {
+ mProtocolVersion.set(params.mProtocolVersion);
+ mSessionId.set(params.mSessionId);
+ mDeviceType.set(params.mDeviceType);
+ mDeviceRole.set(params.mDeviceRole);
+ mRangingRoundUsage = params.mRangingRoundUsage;
+ mMultiNodeMode.set(params.mMultiNodeMode);
+ mDeviceAddress = params.mDeviceAddress;
+ mDestAddressList = params.mDestAddressList;
+ mInitiationTimeMs = params.mInitiationTimeMs;
+ mSlotDurationRstu = params.mSlotDurationRstu;
+ mSlotsPerRangingRound = params.mSlotsPerRangingRound;
+ mRangingIntervalMs = params.mRangingIntervalMs;
+ mBlockStrideLength = params.mBlockStrideLength;
+ mHoppingMode = params.mHoppingMode;
+ mMaxRangingRoundRetries = params.mMaxRangingRoundRetries;
+ mSessionPriority = params.mSessionPriority;
+ mMacAddressMode = params.mMacAddressMode;
+ mHasResultReportPhase = params.mHasResultReportPhase;
+ mMeasurementReportType = params.mMeasurementReportType;
+ mInBandTerminationAttemptCount = params.mInBandTerminationAttemptCount;
+ mChannelNumber = params.mChannelNumber;
+ mPreambleCodeIndex = params.mPreambleCodeIndex;
+ mRframeConfig = params.mRframeConfig;
+ mPrfMode = params.mPrfMode;
+ mPreambleDuration = params.mPreambleDuration;
+ mSfdId = params.mSfdId;
+ mStsSegmentCount = params.mStsSegmentCount;
+ mStsLength = params.mStsLength;
+ mSessionKey = params.mSessionKey;
+ mSubsessionKey = params.mSubSessionKey;
+ mPsduDataRate = params.mPsduDataRate;
+ mBprfPhrDataRate = params.mBprfPhrDataRate;
+ mFcsType = params.mFcsType;
+ mIsTxAdaptivePayloadPowerEnabled = params.mIsTxAdaptivePayloadPowerEnabled;
+ mStsConfig = params.mStsConfig;
+ mSubSessionId.set(params.mSubSessionId);
+ mVendorId = params.mVendorId;
+ mStaticStsIV = params.mStaticStsIV;
+ mIsKeyRotationEnabled = params.mIsKeyRotationEnabled;
+ mKeyRotationRate = params.mKeyRotationRate;
+ mAoaResultRequest = params.mAoaResultRequest;
+ mRangeDataNtfConfig = params.mRangeDataNtfConfig;
+ mRangeDataNtfProximityNear = params.mRangeDataNtfProximityNear;
+ mRangeDataNtfProximityFar = params.mRangeDataNtfProximityFar;
+ mHasTimeOfFlightReport = params.mHasTimeOfFlightReport;
+ mHasAngleOfArrivalAzimuthReport = params.mHasAngleOfArrivalAzimuthReport;
+ mHasAngleOfArrivalElevationReport = params.mHasAngleOfArrivalElevationReport;
+ mHasAngleOfArrivalFigureOfMeritReport = params.mHasAngleOfArrivalFigureOfMeritReport;
+ mAoaType = params.mAoaType;
+ }
+
public FiraOpenSessionParams.Builder setProtocolVersion(FiraProtocolVersion version) {
mProtocolVersion.set(version);
return this;
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java
index acff26c2..7cd1ae49 100644
--- a/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java
@@ -54,6 +54,8 @@ public class FiraSpecificationParams extends FiraParams {
private final boolean mHasInitiationTimeSupport;
+ private final int mMinRangingInterval;
+
private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities;
private final EnumSet<PrfCapabilityFlag> mPrfCapabilities;
@@ -81,6 +83,7 @@ public class FiraSpecificationParams extends FiraParams {
private static final String KEY_BLOCK_STRIDING_SUPPORT = "block_striding";
private static final String KEY_NON_DEFERRED_MODE_SUPPORT = "non_deferred_mode";
private static final String KEY_INITIATION_TIME_SUPPORT = "initiation_time";
+ private static final String KEY_MIN_RANGING_INTERVAL = "min_ranging_interval";
private static final String KEY_MULTI_NODE_CAPABILITIES = "multi_node_capabilities";
private static final String KEY_PRF_CAPABILITIES = "prf_capabilities";
private static final String KEY_RANGING_ROUND_CAPABILITIES = "ranging_round_capabilities";
@@ -103,6 +106,7 @@ public class FiraSpecificationParams extends FiraParams {
boolean hasBlockStridingSupport,
boolean hasNonDeferredModeSupport,
boolean hasInitiationTimeSupport,
+ int minRangingInterval,
EnumSet<MultiNodeCapabilityFlag> multiNodeCapabilities,
EnumSet<PrfCapabilityFlag> prfCapabilities,
EnumSet<RangingRoundCapabilityFlag> rangingRoundCapabilities,
@@ -121,6 +125,7 @@ public class FiraSpecificationParams extends FiraParams {
mHasBlockStridingSupport = hasBlockStridingSupport;
mHasNonDeferredModeSupport = hasNonDeferredModeSupport;
mHasInitiationTimeSupport = hasInitiationTimeSupport;
+ mMinRangingInterval = minRangingInterval;
mMultiNodeCapabilities = multiNodeCapabilities;
mPrfCapabilities = prfCapabilities;
mRangingRoundCapabilities = rangingRoundCapabilities;
@@ -176,6 +181,10 @@ public class FiraSpecificationParams extends FiraParams {
return mHasInitiationTimeSupport;
}
+ public int getMinRangingInterval() {
+ return mMinRangingInterval;
+ }
+
public EnumSet<MultiNodeCapabilityFlag> getMultiNodeCapabilities() {
return mMultiNodeCapabilities;
}
@@ -229,6 +238,7 @@ public class FiraSpecificationParams extends FiraParams {
bundle.putBoolean(KEY_BLOCK_STRIDING_SUPPORT, mHasBlockStridingSupport);
bundle.putBoolean(KEY_NON_DEFERRED_MODE_SUPPORT, mHasNonDeferredModeSupport);
bundle.putBoolean(KEY_INITIATION_TIME_SUPPORT, mHasInitiationTimeSupport);
+ bundle.putInt(KEY_MIN_RANGING_INTERVAL, mMinRangingInterval);
bundle.putInt(KEY_MULTI_NODE_CAPABILITIES, FlagEnum.toInt(mMultiNodeCapabilities));
bundle.putInt(KEY_PRF_CAPABILITIES, FlagEnum.toInt(mPrfCapabilities));
bundle.putInt(KEY_RANGING_ROUND_CAPABILITIES, FlagEnum.toInt(mRangingRoundCapabilities));
@@ -287,6 +297,7 @@ public class FiraSpecificationParams extends FiraParams {
.hasBlockStridingSupport(bundle.getBoolean(KEY_BLOCK_STRIDING_SUPPORT))
.hasNonDeferredModeSupport(bundle.getBoolean(KEY_NON_DEFERRED_MODE_SUPPORT))
.hasInitiationTimeSupport(bundle.getBoolean(KEY_INITIATION_TIME_SUPPORT))
+ .setMinRangingIntervalSupported(bundle.getInt(KEY_MIN_RANGING_INTERVAL, -1))
.setMultiNodeCapabilities(
FlagEnum.toEnumSet(
bundle.getInt(KEY_MULTI_NODE_CAPABILITIES),
@@ -345,6 +356,8 @@ public class FiraSpecificationParams extends FiraParams {
private boolean mHasInitiationTimeSupport = false;
+ private int mMinRangingInterval = -1;
+
// Unicast support is mandatory
private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities =
EnumSet.of(MultiNodeCapabilityFlag.HAS_UNICAST_SUPPORT);
@@ -432,6 +445,16 @@ public class FiraSpecificationParams extends FiraParams {
return this;
}
+ /**
+ * Set minimum supported ranging interval
+ * @param value : minimum ranging interval supported
+ * @return FiraSpecificationParams builder
+ */
+ public FiraSpecificationParams.Builder setMinRangingIntervalSupported(int value) {
+ mMinRangingInterval = value;
+ return this;
+ }
+
public FiraSpecificationParams.Builder setMultiNodeCapabilities(
Collection<MultiNodeCapabilityFlag> multiNodeCapabilities) {
mMultiNodeCapabilities.addAll(multiNodeCapabilities);
@@ -496,6 +519,7 @@ public class FiraSpecificationParams extends FiraParams {
mHasBlockStridingSupport,
mHasNonDeferredModeSupport,
mHasInitiationTimeSupport,
+ mMinRangingInterval,
mMultiNodeCapabilities,
mPrfCapabilities,
mRangingRoundCapabilities,
diff --git a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
index e25f7abc..29e49e2b 100644
--- a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
+++ b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
@@ -42,7 +42,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -166,8 +166,6 @@ public class UwbServiceCoreTest {
when(powerManager.newWakeLock(anyInt(), anyString()))
.thenReturn(mock(PowerManager.WakeLock.class));
when(mContext.getSystemService(PowerManager.class)).thenReturn(powerManager);
- when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
- when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade);
when(mDeviceConfigFacade.getBugReportMinIntervalMs())
.thenReturn(DeviceConfigFacade.DEFAULT_BUG_REPORT_MIN_INTERVAL_MS);
@@ -372,106 +370,6 @@ public class UwbServiceCoreTest {
}
@Test
- public void testOpenRangingWithNonSystemAppInFg() throws Exception {
- enableUwb();
-
- when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
- when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
-
- SessionHandle sessionHandle = mock(SessionHandle.class);
- IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
- AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
- FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
- mUwbServiceCore.openRanging(
- attributionSource, sessionHandle, cb, params.toBundle());
-
- verify(mUwbSessionManager).initSession(
- eq(attributionSource),
- eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME),
- argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()),
- eq(cb));
- }
-
- @Test
- public void testOpenRangingWithNonSystemAppNotInFg() throws Exception {
- enableUwb();
-
- when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
- when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
-
- SessionHandle sessionHandle = mock(SessionHandle.class);
- IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
- AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
- FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
- mUwbServiceCore.openRanging(
- attributionSource, sessionHandle, cb, params.toBundle());
-
- verify(mUwbSessionManager, never()).initSession(
- any(), any(), anyInt(), any(), any(), any());
- verify(cb).onRangingOpenFailed(
- eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any());
- }
-
- @Test
- public void testOpenRangingWithNonSystemAppInFgInChain() throws Exception {
- enableUwb();
-
- int test_uid_2 = 67;
- String test_package_name_2 = "com.android.uwb.2";
- when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
- when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
- .thenReturn(true);
-
- SessionHandle sessionHandle = mock(SessionHandle.class);
- IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
- // simulate system app triggered the request on behalf of a fg app in fg.
- AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID)
- .setPackageName(TEST_PACKAGE_NAME)
- .setNext(new AttributionSource.Builder(test_uid_2)
- .setPackageName(test_package_name_2)
- .build())
- .build();
- FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
- mUwbServiceCore.openRanging(
- attributionSource, sessionHandle, cb, params.toBundle());
-
- verify(mUwbSessionManager).initSession(
- eq(attributionSource),
- eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME),
- argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()),
- eq(cb));
- }
-
- @Test
- public void testOpenRangingWithNonSystemAppNotInFgInChain() throws Exception {
- enableUwb();
-
- int test_uid_2 = 67;
- String test_package_name_2 = "com.android.uwb.2";
- when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
- when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
- .thenReturn(false);
-
- SessionHandle sessionHandle = mock(SessionHandle.class);
- IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
- // simulate system app triggered the request on behalf of a fg app not in fg.
- AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID)
- .setPackageName(TEST_PACKAGE_NAME)
- .setNext(new AttributionSource.Builder(test_uid_2)
- .setPackageName(test_package_name_2)
- .build())
- .build();
- FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
- mUwbServiceCore.openRanging(
- attributionSource, sessionHandle, cb, params.toBundle());
-
- verify(mUwbSessionManager, never()).initSession(
- any(), any(), anyInt(), any(), any(), any());
- verify(cb).onRangingOpenFailed(
- eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any());
- }
-
- @Test
public void testStartCccRanging() throws Exception {
enableUwb();
@@ -652,6 +550,7 @@ public class UwbServiceCoreTest {
StateChangeReason.SYSTEM_POLICY);
when(mNativeUwbManager.doDeinitialize()).thenReturn(true);
+ when(mNativeUwbManager.doInitialize()).thenReturn(true);
mUwbServiceCore.onDeviceStatusNotificationReceived(UwbUciConstants.DEVICE_STATE_ERROR);
mTestLooper.dispatchAll();
@@ -659,6 +558,12 @@ public class UwbServiceCoreTest {
verify(mNativeUwbManager).doDeinitialize();
verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
StateChangeReason.SYSTEM_POLICY);
+
+ // Verify UWB toggle on.
+ verify(mNativeUwbManager, times(2)).doInitialize();
+ verify(cb, times(2)).onAdapterStateChanged(
+ UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+ StateChangeReason.SYSTEM_POLICY);
}
@Test
diff --git a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
index cb0ca82e..bbe028c9 100644
--- a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
+++ b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.uwb;
+import static com.android.server.uwb.UwbSessionManager.SESSION_OPEN_RANGING;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -43,6 +45,7 @@ import android.os.test.TestLooper;
import android.uwb.IUwbRangingCallbacks;
import android.uwb.RangingChangeReason;
import android.uwb.SessionHandle;
+import android.uwb.StateChangeReason;
import android.uwb.UwbAddress;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
@@ -110,6 +113,8 @@ public class UwbSessionManagerTest {
public void setup() {
MockitoAnnotations.initMocks(this);
when(mNativeUwbManager.getMaxSessionNumber()).thenReturn(MAX_SESSION_NUM);
+ when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(true);
+ when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(true);
// TODO: Don't use spy.
mUwbSessionManager = spy(new UwbSessionManager(
@@ -608,25 +613,7 @@ public class UwbSessionManagerTest {
assertThat(actualStatus).isEqualTo(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST);
}
- @Test
- public void reconfigure_calledSuccess() {
- doReturn(true).when(mUwbSessionManager).isExistedSession(any());
- FiraRangingReconfigureParams params =
- new FiraRangingReconfigureParams.Builder()
- .setBlockStrideLength(10)
- .setRangeDataNtfConfig(1)
- .setRangeDataProximityFar(10)
- .setRangeDataProximityNear(2)
- .build();
-
- int actualStatus = mUwbSessionManager.reconfigure(mock(SessionHandle.class), params);
-
- assertThat(actualStatus).isEqualTo(0);
- assertThat(mTestLooper.nextMessage().what)
- .isEqualTo(4); // SESSION_RECONFIG_RANGING
- }
-
- private UwbSession setUpUwbSessionForExecution() throws RemoteException {
+ private UwbSession setUpUwbSessionForExecution(AttributionSource attributionSource) {
// setup message
doReturn(0).when(mUwbSessionManager).getSessionCount();
doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
@@ -647,7 +634,7 @@ public class UwbSessionManagerTest {
.build();
IBinder mockBinder = mock(IBinder.class);
UwbSession uwbSession = spy(
- mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle,
+ mUwbSessionManager.new UwbSession(attributionSource, mockSessionHandle,
TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, params, mockRangingCallbacks));
doReturn(mockBinder).when(uwbSession).getBinder();
doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(),
@@ -694,7 +681,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_success() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
@@ -718,7 +705,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_timeout() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenThrow(new IllegalStateException());
@@ -742,7 +729,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_nativeInitSessionFailed() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
@@ -766,7 +753,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_setAppConfigurationFailed() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
@@ -790,7 +777,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_wrongInitState() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
@@ -814,7 +801,7 @@ public class UwbSessionManagerTest {
@Test
public void openRanging_wrongIdleState() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
// stub for openRanging conditions
when(mNativeUwbManager.initSession(anyInt(), anyByte()))
.thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
@@ -837,8 +824,92 @@ public class UwbSessionManagerTest {
verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
}
+ @Test
+ public void testInitSessionWithNonSystemAppInFg() throws Exception {
+ when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(false);
+ when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(true);
+
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
+ mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+ TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+ uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+
+ // OPEN_RANGING message scheduled.
+ assertThat(mTestLooper.nextMessage().what).isEqualTo(SESSION_OPEN_RANGING);
+ assertThat(mTestLooper.isIdle()).isFalse();
+ }
+
+ @Test
+ public void testInitSessionWithNonSystemAppNotInFg() throws Exception {
+ when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(false);
+ when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(false);
+
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
+ mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+ TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+ uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+
+ verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed(
+ eq(uwbSession.getSessionHandle()), eq(StateChangeReason.SYSTEM_POLICY), any());
+ // No OPEN_RANGING message scheduled.
+ assertThat(mTestLooper.isIdle()).isFalse();
+ }
+
+ @Test
+ public void testOpenRangingWithNonSystemAppInFgInChain() throws Exception {
+ int test_uid_2 = 67;
+ String test_package_name_2 = "com.android.uwb.2";
+ when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
+ when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
+ .thenReturn(true);
+
+ // simulate system app triggered the request on behalf of a fg app in fg.
+ AttributionSource attributionSource = new AttributionSource.Builder(UID)
+ .setPackageName(PACKAGE_NAME)
+ .setNext(new AttributionSource.Builder(test_uid_2)
+ .setPackageName(test_package_name_2)
+ .build())
+ .build();
+
+ UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource);
+
+ mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(),
+ TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+ uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+
+ // OPEN_RANGING message scheduled.
+ assertThat(mTestLooper.nextMessage().what).isEqualTo(SESSION_OPEN_RANGING);
+ assertThat(mTestLooper.isIdle()).isFalse();
+ }
+
+ @Test
+ public void testOpenRangingWithNonSystemAppNotInFgInChain() throws Exception {
+ int test_uid_2 = 67;
+ String test_package_name_2 = "com.android.uwb.2";
+ when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
+ when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
+ .thenReturn(false);
+
+ // simulate system app triggered the request on behalf of a fg app not in fg.
+ AttributionSource attributionSource = new AttributionSource.Builder(UID)
+ .setPackageName(PACKAGE_NAME)
+ .setNext(new AttributionSource.Builder(test_uid_2)
+ .setPackageName(test_package_name_2)
+ .build())
+ .build();
+ UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource);
+ mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(),
+ TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+ uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+
+ verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed(
+ eq(uwbSession.getSessionHandle()), eq(StateChangeReason.SYSTEM_POLICY), any());
+ // No OPEN_RANGING message scheduled.
+ assertThat(mTestLooper.isIdle()).isFalse();
+ }
+
private UwbSession prepareExistingUwbSession() throws Exception {
- UwbSession uwbSession = setUpUwbSessionForExecution();
+ UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE);
mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
@@ -862,6 +933,31 @@ public class UwbSessionManagerTest {
}
@Test
+ public void reconfigure_calledSuccess() throws Exception {
+ UwbSession uwbSession = prepareExistingUwbSession();
+ FiraRangingReconfigureParams params =
+ new FiraRangingReconfigureParams.Builder()
+ .setBlockStrideLength(10)
+ .setRangeDataNtfConfig(1)
+ .setRangeDataProximityFar(10)
+ .setRangeDataProximityNear(2)
+ .build();
+
+ int actualStatus = mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), params);
+
+ assertThat(actualStatus).isEqualTo(0);
+ assertThat(mTestLooper.nextMessage().what)
+ .isEqualTo(UwbSessionManager.SESSION_RECONFIG_RANGING);
+
+ // Verify the cache has been updated.
+ FiraOpenSessionParams firaParams = (FiraOpenSessionParams) uwbSession.getParams();
+ assertThat(firaParams.getBlockStrideLength()).isEqualTo(10);
+ assertThat(firaParams.getRangeDataNtfConfig()).isEqualTo(1);
+ assertThat(firaParams.getRangeDataNtfProximityFar()).isEqualTo(10);
+ assertThat(firaParams.getRangeDataNtfProximityNear()).isEqualTo(2);
+ }
+
+ @Test
public void startRanging_sessionStateIdle() throws Exception {
UwbSession uwbSession = prepareExistingUwbSession();
// set up for start ranging
diff --git a/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java b/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java
new file mode 100644
index 00000000..40603ef4
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothManager;
+import android.content.AttributionSource;
+import android.content.Context;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
+import com.android.server.uwb.discovery.info.DiscoveryInfo;
+import com.android.server.uwb.discovery.info.DiscoveryInfo.TransportType;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/** Unit test for {@link TransportServerService} */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TransportServerServiceTest {
+
+ private static final DiscoveryInfo DISCOVERY_INFO =
+ new DiscoveryInfo(TransportType.BLE, Optional.empty(), Optional.empty());
+
+ @Mock AttributionSource mMockAttributionSource;
+ @Mock Context mMockContext;
+ @Mock BluetoothManager mMockBluetoothManager;
+ @Mock BluetoothAdapter mMockBluetoothAdapter;
+ @Mock BluetoothGattServer mMockBluetoothGattServer;
+ @Mock TransportServerCallback mMockTransportServerCallback;
+
+ private TransportServerService mTransportServerService;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mMockContext.createContext(any())).thenReturn(mMockContext);
+ when(mMockContext.getSystemService(BluetoothManager.class))
+ .thenReturn(mMockBluetoothManager);
+ when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter);
+ when(mMockBluetoothManager.openGattServer(
+ eq(mMockContext), any(BluetoothGattServerCallback.class)))
+ .thenReturn(mMockBluetoothGattServer);
+
+ mTransportServerService =
+ new TransportServerService(
+ mMockAttributionSource,
+ mMockContext,
+ DISCOVERY_INFO,
+ mMockTransportServerCallback);
+ }
+
+ @Test
+ public void testStart_failed() {
+ when(mMockBluetoothGattServer.addService(any())).thenReturn(false);
+ assertThat(mTransportServerService.start()).isFalse();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ }
+
+ @Test
+ public void testStart_successAndRejectRestart() {
+ when(mMockBluetoothGattServer.addService(any())).thenReturn(true);
+ assertThat(mTransportServerService.start()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ assertThat(mTransportServerService.start()).isFalse();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ }
+
+ @Test
+ public void testStop_failed() {
+ when(mMockBluetoothGattServer.addService(any())).thenReturn(true);
+ when(mMockBluetoothGattServer.removeService(any())).thenReturn(false);
+ assertThat(mTransportServerService.start()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ assertThat(mTransportServerService.stop()).isFalse();
+ verify(mMockBluetoothGattServer, times(1)).removeService(any());
+ }
+
+ @Test
+ public void testStop_successAndRejectRestop() {
+ when(mMockBluetoothGattServer.addService(any())).thenReturn(true);
+ when(mMockBluetoothGattServer.removeService(any())).thenReturn(true);
+ assertThat(mTransportServerService.start()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ assertThat(mTransportServerService.stop()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).removeService(any());
+ assertThat(mTransportServerService.stop()).isFalse();
+ verify(mMockBluetoothGattServer, times(1)).removeService(any());
+ }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java
new file mode 100644
index 00000000..cb328dd5
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+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.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.uwb.UwbTestUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
+import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
+import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage.InstructionCode;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType;
+import com.android.server.uwb.discovery.info.SecureComponentInfo;
+
+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.Arrays;
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link GattTransportServerProvider} */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GattTransportServerProviderTest {
+
+ private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
+ private static final int SECID = 10;
+ private static final byte[] MESSAGE_PAYLOAD1 = new byte[] {(byte) 0xF4, 0x00, 0x40};
+ private static final FiraConnectorMessage MESSAGE =
+ new FiraConnectorMessage(
+ MessageType.EVENT, InstructionCode.DATA_EXCHANGE, MESSAGE_PAYLOAD1);
+ private static final FiraConnectorDataPacket DATA_PACKET =
+ new FiraConnectorDataPacket(/*lastChainingPacket=*/ true, SECID, MESSAGE.toBytes());
+ private static final int OPTIMIZED_DATA_PACKET_SIZE = 21;
+ private static final FiraConnectorCapabilities CAPABILITIES =
+ new FiraConnectorCapabilities.Builder()
+ .setOptimizedDataPacketSize(OPTIMIZED_DATA_PACKET_SIZE)
+ .setMaxMessageBufferSize(265)
+ .addSecureComponentInfo(
+ new SecureComponentInfo(
+ /*static_indication=*/ true,
+ SECID,
+ SecureComponentInfo.SecureComponentType.ESE_NONREMOVABLE,
+ SecureComponentInfo.SecureComponentProtocolType
+ .FIRA_OOB_ADMINISTRATIVE_PROTOCOL))
+ .build();
+ private static final BluetoothGattCharacteristic IN_CHARACTERSTIC =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+ private static final BluetoothGattCharacteristic OUT_CHARACTERSTIC =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_READ
+ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+ BluetoothGattCharacteristic.PERMISSION_READ);
+ private static final BluetoothGattCharacteristic CAPABILITIES_CHARACTERSTIC =
+ new BluetoothGattCharacteristic(
+ UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(),
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+ private static final BluetoothGattDescriptor CCCD_DESCRIPTOR =
+ new BluetoothGattDescriptor(
+ UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ);
+
+ @Mock AttributionSource mMockAttributionSource;
+ @Mock Context mMockContext;
+ @Mock BluetoothManager mMockBluetoothManager;
+ @Mock BluetoothAdapter mMockBluetoothAdapter;
+ @Mock BluetoothGattServer mMockBluetoothGattServer;
+ @Mock TransportServerCallback mMockTransportServerCallback;
+ @Mock BluetoothDevice mMockBluetoothDevice;
+
+ private GattTransportServerProvider mGattTransportServerProvider;
+ private BluetoothGattServerCallback mBluetoothGattServerCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mMockContext.createContext(any())).thenReturn(mMockContext);
+ when(mMockContext.getSystemService(BluetoothManager.class))
+ .thenReturn(mMockBluetoothManager);
+ when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter);
+ when(mMockBluetoothManager.openGattServer(eq(mMockContext), any()))
+ .thenReturn(mMockBluetoothGattServer);
+ when(mMockBluetoothGattServer.addService(any())).thenReturn(true);
+ when(mMockBluetoothGattServer.removeService(any())).thenReturn(true);
+
+ mGattTransportServerProvider =
+ new GattTransportServerProvider(
+ mMockAttributionSource, mMockContext, mMockTransportServerCallback);
+
+ ArgumentCaptor<BluetoothGattServerCallback> captor =
+ ArgumentCaptor.forClass(BluetoothGattServerCallback.class);
+ verify(mMockBluetoothManager, times(1)).openGattServer(eq(mMockContext), captor.capture());
+ mBluetoothGattServerCallback = captor.getValue();
+ assertThat(mBluetoothGattServerCallback).isNotNull();
+ }
+
+ @Test
+ public void testStartAndStop() {
+ assertThat(mGattTransportServerProvider.start()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).addService(any());
+ assertThat(mGattTransportServerProvider.isStarted()).isTrue();
+ assertThat(mGattTransportServerProvider.stop()).isTrue();
+ verify(mMockBluetoothGattServer, times(1)).removeService(any());
+ assertThat(mGattTransportServerProvider.isStarted()).isFalse();
+ }
+
+ @Test
+ public void testStartProcessing_succeed() {
+ mBluetoothGattServerCallback.onConnectionStateChange(
+ mMockBluetoothDevice, /*status=*/ 1, BluetoothProfile.STATE_CONNECTED);
+ mBluetoothGattServerCallback.onDescriptorWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 1,
+ CCCD_DESCRIPTOR,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 2,
+ CAPABILITIES_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ CAPABILITIES.toBytes());
+
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 1,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 2,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ CAPABILITIES.toBytes());
+ ArgumentCaptor<FiraConnectorCapabilities> captor =
+ ArgumentCaptor.forClass(FiraConnectorCapabilities.class);
+ verify(mMockTransportServerCallback, times(1)).onCapabilitesUpdated(captor.capture());
+ assertThat(captor.getValue().toString()).isEqualTo(CAPABILITIES.toString());
+ verify(mMockTransportServerCallback, times(1)).onProcessingStarted();
+ verify(mMockTransportServerCallback, never()).onProcessingStopped();
+ }
+
+ private void startProcessing() {
+ mBluetoothGattServerCallback.onConnectionStateChange(
+ mMockBluetoothDevice, /*status=*/ 1, BluetoothProfile.STATE_CONNECTED);
+ mBluetoothGattServerCallback.onDescriptorWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 1,
+ CCCD_DESCRIPTOR,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ false,
+ /*offset=*/ 0,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 2,
+ CAPABILITIES_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ false,
+ /*offset=*/ 0,
+ CAPABILITIES.toBytes());
+ }
+
+ @Test
+ public void testSendMessage_succeed() {
+ when(mMockBluetoothGattServer.notifyCharacteristicChanged(
+ eq(mMockBluetoothDevice), any(), eq(false)))
+ .thenReturn(true);
+
+ startProcessing();
+
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isTrue();
+ }
+
+ @Test
+ public void testSendMessage_failedProcessingNotStarted() {
+ when(mMockBluetoothGattServer.notifyCharacteristicChanged(
+ eq(mMockBluetoothDevice), any(), eq(false)))
+ .thenReturn(true);
+
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isFalse();
+ }
+
+ @Test
+ public void testSendMessage_failedMessageLengthGreaterThanCapabilitites() {
+ when(mMockBluetoothGattServer.notifyCharacteristicChanged(
+ eq(mMockBluetoothDevice), any(), eq(false)))
+ .thenReturn(true);
+ byte[] bytes = new byte[270];
+ Arrays.fill(bytes, (byte) 1);
+ FiraConnectorMessage message =
+ new FiraConnectorMessage(MessageType.EVENT, InstructionCode.DATA_EXCHANGE, bytes);
+
+ startProcessing();
+
+ // Capabilities set the max message length to 265, so a message of length 270 exceeded the
+ // limit.
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, message)).isFalse();
+ }
+
+ @Test
+ public void testSendMessage_failedNotifyCharacteristicChangedFailed() {
+ when(mMockBluetoothGattServer.notifyCharacteristicChanged(
+ eq(mMockBluetoothDevice), any(), eq(false)))
+ .thenReturn(false);
+
+ startProcessing();
+
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isFalse();
+ }
+
+ private void setupOutCharactersticRead() {
+ Answer notifyOutCharacteristicChangedResponse =
+ new Answer() {
+ public Object answer(InvocationOnMock invocation) {
+ EXECUTOR.execute(
+ () ->
+ mBluetoothGattServerCallback.onCharacteristicReadRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 3,
+ /*offset=*/ 0,
+ OUT_CHARACTERSTIC));
+ return true;
+ }
+ };
+ doAnswer(notifyOutCharacteristicChangedResponse)
+ .when(mMockBluetoothGattServer)
+ .notifyCharacteristicChanged(eq(mMockBluetoothDevice), any(), eq(false));
+ }
+
+ @Test
+ public void testSendMessageAndOutCharactersticRead_succeed() {
+ setupOutCharactersticRead();
+ startProcessing();
+
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isTrue();
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 3,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ DATA_PACKET.toBytes());
+ }
+
+ @Test
+ public void testSendMessageAndOutCharactersticRead_threeReadSucceed() {
+ byte[] messagePayload = new byte[45];
+ Arrays.fill(messagePayload, (byte) 2);
+ FiraConnectorMessage message =
+ new FiraConnectorMessage(
+ MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload);
+ byte[] messageBytes = message.toBytes();
+ int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1;
+ FiraConnectorDataPacket dataPacket1 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ false,
+ SECID,
+ Arrays.copyOf(messageBytes, payloadSize));
+ FiraConnectorDataPacket dataPacket2 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ false,
+ SECID,
+ Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize));
+ FiraConnectorDataPacket dataPacket3 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ true,
+ SECID,
+ Arrays.copyOfRange(messageBytes, 2 * payloadSize, messageBytes.length));
+
+ setupOutCharactersticRead();
+ startProcessing();
+
+ assertThat(mGattTransportServerProvider.sendMessage(SECID, message)).isTrue();
+ verify(mMockBluetoothGattServer, times(3))
+ .sendResponse(
+ eq(mMockBluetoothDevice),
+ eq(/*requestId=*/ 3),
+ eq(BluetoothGatt.GATT_SUCCESS),
+ eq(/*offset=*/ 0),
+ any());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 3,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket1.toBytes());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 3,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket2.toBytes());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 3,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket3.toBytes());
+ }
+
+ @Test
+ public void testInCharactersticWrite_failedProcessingNotStarted() {
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ DATA_PACKET.toBytes());
+
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ BluetoothGatt.GATT_FAILURE,
+ /*offset=*/ 0,
+ /*value=*/ null);
+ verifyZeroInteractions(mMockTransportServerCallback);
+ }
+
+ @Test
+ public void testInCharactersticWrite_failedEmptyDataPacket() {
+ startProcessing();
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ new byte[] {});
+
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ BluetoothGatt.GATT_FAILURE,
+ /*offset=*/ 0,
+ /*value=*/ null);
+ verify(mMockTransportServerCallback, never()).onMessage(anyInt(), any());
+ }
+
+ @Test
+ public void testInCharactersticWrite_noResponse() {
+ startProcessing();
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ false,
+ /*offset=*/ 0,
+ new byte[] {});
+
+ verify(mMockBluetoothGattServer, never())
+ .sendResponse(any(BluetoothDevice.class), anyInt(), anyInt(), anyInt(), any());
+ verify(mMockTransportServerCallback, never()).onMessage(anyInt(), any());
+
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ false,
+ /*offset=*/ 0,
+ DATA_PACKET.toBytes());
+
+ verify(mMockBluetoothGattServer, never())
+ .sendResponse(any(BluetoothDevice.class), anyInt(), anyInt(), anyInt(), any());
+ ArgumentCaptor<FiraConnectorMessage> captor =
+ ArgumentCaptor.forClass(FiraConnectorMessage.class);
+ verify(mMockTransportServerCallback, times(1)).onMessage(eq(SECID), captor.capture());
+ assertThat(captor.getValue().toString()).isEqualTo(MESSAGE.toString());
+ }
+
+ @Test
+ public void testInCharactersticWrite_succeed() {
+ byte[] messagePayload = new byte[51];
+ Arrays.fill(messagePayload, (byte) 3);
+ FiraConnectorMessage message =
+ new FiraConnectorMessage(
+ MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload);
+ byte[] messageBytes = message.toBytes();
+ int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1;
+ FiraConnectorDataPacket dataPacket1 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ false,
+ SECID,
+ Arrays.copyOf(messageBytes, payloadSize));
+ FiraConnectorDataPacket dataPacket2 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ false,
+ SECID,
+ Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize));
+ FiraConnectorDataPacket dataPacket3 =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ true,
+ SECID,
+ Arrays.copyOfRange(messageBytes, 2 * payloadSize, messageBytes.length));
+
+ startProcessing();
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ dataPacket1.toBytes());
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ dataPacket2.toBytes());
+ mBluetoothGattServerCallback.onCharacteristicWriteRequest(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ IN_CHARACTERSTIC,
+ /*preparedWrite=*/ false,
+ /*responseNeeded=*/ true,
+ /*offset=*/ 0,
+ dataPacket3.toBytes());
+
+ verify(mMockBluetoothGattServer, times(3))
+ .sendResponse(
+ eq(mMockBluetoothDevice),
+ eq(/*requestId=*/ 4),
+ eq(BluetoothGatt.GATT_SUCCESS),
+ eq(/*offset=*/ 0),
+ any());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket1.toBytes());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket2.toBytes());
+ verify(mMockBluetoothGattServer, times(1))
+ .sendResponse(
+ mMockBluetoothDevice,
+ /*requestId=*/ 4,
+ BluetoothGatt.GATT_SUCCESS,
+ /*offset=*/ 0,
+ dataPacket3.toBytes());
+ ArgumentCaptor<FiraConnectorMessage> captor =
+ ArgumentCaptor.forClass(FiraConnectorMessage.class);
+ verify(mMockTransportServerCallback, times(1)).onMessage(eq(SECID), captor.capture());
+ assertThat(captor.getValue().toString()).isEqualTo(message.toString());
+ }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java
index 44810674..6e1f9762 100644
--- a/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java
+++ b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java
@@ -87,10 +87,11 @@ public class FiraDecoderTest {
+ "0F050000000003"
+ "10010F"
+ "110101"
- + "E30101";
+ + "E30101"
+ + "E40401010101";
private static final byte[] TEST_FIRA_SPECIFICATION_TLV_DATA =
UwbUtil.getByteArray(TEST_FIRA_SPECIFICATION_TLV_STRING);
- public static final int TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS = 19;
+ public static final int TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS = 20;
private final FiraDecoder mFiraDecoder = new FiraDecoder();
public static void verifyFiraSpecification(FiraSpecificationParams firaSpecificationParams) {