[ranging] Implement CS support for OOB Initiator
Bug: 372106978
Test: atest MultiDeviceRangingTestCases
Change-Id: Ib20dab6f15046bccc329b00656bcf1cc8d04514e
diff --git a/ranging/framework/java/android/ranging/ble/cs/BleCsRangingCapabilities.java b/ranging/framework/java/android/ranging/ble/cs/BleCsRangingCapabilities.java
index 08341ab..66d659e 100644
--- a/ranging/framework/java/android/ranging/ble/cs/BleCsRangingCapabilities.java
+++ b/ranging/framework/java/android/ranging/ble/cs/BleCsRangingCapabilities.java
@@ -26,8 +26,10 @@
import com.android.ranging.flags.Flags;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -42,6 +44,7 @@
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.TYPE_USE})
@IntDef({
CS_SECURITY_LEVEL_ONE,
CS_SECURITY_LEVEL_FOUR,
diff --git a/ranging/service/java/com/android/server/ranging/RangingEngine.java b/ranging/service/java/com/android/server/ranging/RangingEngine.java
index a7215b9..95f1faa 100644
--- a/ranging/service/java/com/android/server/ranging/RangingEngine.java
+++ b/ranging/service/java/com/android/server/ranging/RangingEngine.java
@@ -25,12 +25,17 @@
import android.ranging.RangingDevice;
import android.ranging.SessionConfig;
import android.ranging.SessionHandle;
+import android.ranging.ble.cs.BleCsRangingCapabilities;
import android.ranging.oob.OobInitiatorRangingConfig;
import android.ranging.uwb.UwbRangingCapabilities;
import android.util.Log;
import androidx.annotation.Nullable;
+import com.android.server.ranging.cs.CsConfigSelector;
+import com.android.server.ranging.cs.CsConfigSelector.SelectedCsConfig;
+import com.android.server.ranging.cs.CsOobCapabilities;
+import com.android.server.ranging.cs.CsOobConfig;
import com.android.server.ranging.oob.CapabilityResponseMessage;
import com.android.server.ranging.oob.MessageType;
import com.android.server.ranging.oob.OobHeader;
@@ -57,7 +62,8 @@
private final EnumSet<RangingTechnology> mRequestedTechnologies;
private final Map<RangingDevice, EnumSet<RangingTechnology>> mPeerTechnologies;
- private final @Nullable UwbConfigSelector mUwbConfigSelector;
+ private @Nullable UwbConfigSelector mUwbConfigSelector = null;
+ private @Nullable CsConfigSelector mCsConfigSelector = null;
public static class ConfigSelectionException extends Exception {
public ConfigSelectionException(String message) {
@@ -102,12 +108,14 @@
mRequestedTechnologies.add(RangingTechnology.UWB);
mUwbConfigSelector = new UwbConfigSelector(
sessionConfig, oobConfig, uwbCapabilities, sessionHandle);
- } else {
- mUwbConfigSelector = null;
}
if (oobConfig.getRangingMode() != RANGING_MODE_HIGH_ACCURACY) {
- // TODO: Other technologies
+ BleCsRangingCapabilities csCapabilities = localCapabilities.getCsCapabilities();
+ if (CsConfigSelector.isCapableOfConfig(oobConfig, csCapabilities)) {
+ mRequestedTechnologies.add(RangingTechnology.CS);
+ mCsConfigSelector = new CsConfigSelector(sessionConfig, oobConfig);
+ }
}
if (mRequestedTechnologies.isEmpty()) {
@@ -126,14 +134,21 @@
EnumSet<RangingTechnology> selectedTechnologies =
selectTechnologiesToUseWithPeer(capabilities);
+
UwbOobCapabilities uwbCapabilities = capabilities.getUwbCapabilities();
+ CsOobCapabilities csCapabilities = capabilities.getCsCapabilities();
+
for (RangingTechnology technology : selectedTechnologies) {
- // TODO: Other technologies
if (technology == RangingTechnology.UWB
&& uwbCapabilities != null
&& mUwbConfigSelector != null
) {
mUwbConfigSelector.restrictConfigToCapabilities(device, uwbCapabilities);
+ } else if (technology == RangingTechnology.CS
+ && csCapabilities != null
+ && mCsConfigSelector != null
+ ) {
+ mCsConfigSelector.restrictConfigToCapabilities(device, csCapabilities);
} else {
Log.e(TAG, "Technology " + technology + " was selected by us and peer " + device
+ ", but one of us does not actually support it");
@@ -146,33 +161,42 @@
public SelectedConfig selectConfigs() throws ConfigSelectionException {
ImmutableSet.Builder<TechnologyConfig> localConfigs = ImmutableSet.builder();
- ImmutableMap.Builder<RangingDevice, SetConfigurationMessage> peerConfigs =
+ ImmutableMap.Builder<RangingDevice, SetConfigurationMessage> configMessages =
ImmutableMap.builder();
Map<RangingDevice, UwbOobConfig> uwbConfigsByPeer = new HashMap<>();
- if (mUwbConfigSelector != null) {
+ if (mUwbConfigSelector != null && mUwbConfigSelector.hasPeersToConfigure()) {
SelectedUwbConfig uwbConfig = mUwbConfigSelector.selectConfig();
localConfigs.add(uwbConfig.getLocalConfig());
uwbConfigsByPeer.putAll(uwbConfig.getPeerConfigs());
}
+ Map<RangingDevice, CsOobConfig> csConfigsByPeer = new HashMap<>();
+ if (mCsConfigSelector != null && mCsConfigSelector.hasPeersToConfigure()) {
+ SelectedCsConfig csConfig = mCsConfigSelector.selectConfig();
+ localConfigs.addAll(csConfig.getLocalConfigs());
+ csConfigsByPeer.putAll(csConfig.getPeerConfigs());
+ }
+
for (RangingDevice peer : mPeerTechnologies.keySet()) {
+ ImmutableList<RangingTechnology> peerTechnologies =
+ ImmutableList.copyOf(mPeerTechnologies.get(peer));
+
SetConfigurationMessage.Builder configMessage = SetConfigurationMessage.builder()
.setHeader(OobHeader.builder()
.setMessageType(MessageType.SET_CONFIGURATION)
.setVersion(OobHeader.OobVersion.CURRENT)
.build())
- .setRangingTechnologiesSet(ImmutableList.copyOf(mRequestedTechnologies))
- .setStartRangingList(ImmutableList.copyOf(mRequestedTechnologies));
+ .setRangingTechnologiesSet(peerTechnologies)
+ .setStartRangingList(peerTechnologies);
- if (uwbConfigsByPeer.containsKey(peer)) {
- configMessage.setUwbConfig(uwbConfigsByPeer.get(peer));
- }
+ configMessage.setUwbConfig(uwbConfigsByPeer.get(peer));
+ configMessage.setCsConfig(csConfigsByPeer.get(peer));
- peerConfigs.put(peer, configMessage.build());
+ configMessages.put(peer, configMessage.build());
}
- return new SelectedConfig(localConfigs.build(), peerConfigs.build());
+ return new SelectedConfig(localConfigs.build(), configMessages.build());
}
private EnumSet<RangingTechnology> selectTechnologiesToUseWithPeer(
diff --git a/ranging/service/java/com/android/server/ranging/cs/CsConfig.java b/ranging/service/java/com/android/server/ranging/cs/CsConfig.java
index ee81011..0dd586b 100644
--- a/ranging/service/java/com/android/server/ranging/cs/CsConfig.java
+++ b/ranging/service/java/com/android/server/ranging/cs/CsConfig.java
@@ -16,18 +16,35 @@
package com.android.server.ranging.cs;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_FREQUENT;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_INFREQUENT;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_NORMAL;
+
import android.annotation.NonNull;
import android.ranging.RangingDevice;
import android.ranging.RangingPreference;
import android.ranging.SessionConfig;
import android.ranging.ble.cs.BleCsRangingParams;
+import android.ranging.raw.RawRangingDevice;
import com.android.server.ranging.RangingTechnology;
import com.android.server.ranging.session.RangingSessionConfig.UnicastTechnologyConfig;
+import com.google.common.collect.ImmutableMap;
+
+import java.time.Duration;
+
public class CsConfig implements UnicastTechnologyConfig {
private static final String TAG = CsConfig.class.getSimpleName();
+ // TODO(390665219): Update this once we decide on a set of measurement intervals for channel
+ // sounding.
+ public static final ImmutableMap<@RawRangingDevice.RangingUpdateRate Integer, Duration>
+ CS_UPDATE_RATE_DURATIONS = ImmutableMap.of(
+ UPDATE_RATE_NORMAL, Duration.ofSeconds(3),
+ UPDATE_RATE_INFREQUENT, Duration.ofSeconds(5),
+ UPDATE_RATE_FREQUENT, Duration.ofMillis(200));
+
private final SessionConfig mSessionConfig;
private final BleCsRangingParams mRangingParams;
diff --git a/ranging/service/java/com/android/server/ranging/cs/CsConfigSelector.java b/ranging/service/java/com/android/server/ranging/cs/CsConfigSelector.java
new file mode 100644
index 0000000..3204904
--- /dev/null
+++ b/ranging/service/java/com/android/server/ranging/cs/CsConfigSelector.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2025 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.ranging.cs;
+
+import static android.ranging.RangingPreference.DEVICE_ROLE_INITIATOR;
+import static android.ranging.ble.cs.BleCsRangingCapabilities.CS_SECURITY_LEVEL_FOUR;
+import static android.ranging.ble.cs.BleCsRangingCapabilities.CS_SECURITY_LEVEL_ONE;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_FREQUENT;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_INFREQUENT;
+import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_NORMAL;
+
+import static com.android.server.ranging.cs.CsConfig.CS_UPDATE_RATE_DURATIONS;
+
+import android.ranging.RangingDevice;
+import android.ranging.SessionConfig;
+import android.ranging.ble.cs.BleCsRangingCapabilities;
+import android.ranging.ble.cs.BleCsRangingParams;
+import android.ranging.oob.OobInitiatorRangingConfig;
+import android.ranging.raw.RawRangingDevice;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.server.ranging.RangingEngine.ConfigSelectionException;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+public class CsConfigSelector {
+ private final SessionConfig mSessionConfig;
+ private final OobInitiatorRangingConfig mOobConfig;
+ private final BiMap<RangingDevice, String> mPeerAddresses;
+ private final Map<RangingDevice, @BleCsRangingCapabilities.SecurityLevel Integer>
+ mPeerSecurityLevels;
+
+ public static boolean isCapableOfConfig(
+ @NonNull OobInitiatorRangingConfig oobConfig,
+ @Nullable BleCsRangingCapabilities capabilities
+ ) {
+ if (capabilities == null) return false;
+
+ if (!(capabilities.getSupportedSecurityLevels().contains(CS_SECURITY_LEVEL_ONE)
+ || capabilities.getSupportedSecurityLevels().contains(CS_SECURITY_LEVEL_FOUR))
+ ) return false;
+
+ if (getDurationFromUpdateRate(oobConfig.getRangingIntervalRange()).isEmpty()) return false;
+
+ return true;
+ }
+
+ public CsConfigSelector(
+ @NonNull SessionConfig sessionConfig,
+ @NonNull OobInitiatorRangingConfig oobConfig
+ ) {
+ mSessionConfig = sessionConfig;
+ mOobConfig = oobConfig;
+ mPeerAddresses = HashBiMap.create();
+ mPeerSecurityLevels = new HashMap<>();
+ }
+
+ public void restrictConfigToCapabilities(
+ @NonNull RangingDevice peer, @NonNull CsOobCapabilities capabilities
+ ) throws ConfigSelectionException {
+ mPeerAddresses.put(peer, capabilities.getBluetoothAddress());
+
+ if (mOobConfig.getSecurityLevel() == OobInitiatorRangingConfig.SECURITY_LEVEL_BASIC) {
+ if (capabilities.getSupportedSecurityTypes()
+ .contains(CsOobConfig.CsSecurityType.LEVEL_ONE)
+ ) {
+ mPeerSecurityLevels.put(peer, CS_SECURITY_LEVEL_ONE);
+ } else {
+ throw new ConfigSelectionException("Configured security level "
+ + OobInitiatorRangingConfig.SECURITY_LEVEL_BASIC + " but " + peer
+ + " only supports " + capabilities.getSupportedSecurityTypes());
+ }
+ } else {
+ if (capabilities.getSupportedSecurityTypes()
+ .contains(CsOobConfig.CsSecurityType.LEVEL_FOUR)
+ ) {
+ mPeerSecurityLevels.put(peer, CS_SECURITY_LEVEL_FOUR);
+ } else if (capabilities.getSupportedSecurityTypes()
+ .contains(CsOobConfig.CsSecurityType.LEVEL_ONE)
+ ) {
+ mPeerSecurityLevels.put(peer, CS_SECURITY_LEVEL_ONE);
+ } else {
+ throw new ConfigSelectionException("Configured security level "
+ + OobInitiatorRangingConfig.SECURITY_LEVEL_SECURE + " but " + peer
+ + " only supports " + capabilities.getSupportedSecurityTypes());
+ }
+ }
+ }
+
+ public boolean hasPeersToConfigure() {
+ return !mPeerAddresses.isEmpty();
+ }
+
+ public @NonNull SelectedCsConfig selectConfig() throws ConfigSelectionException {
+ return new SelectedCsConfig();
+ }
+
+ public class SelectedCsConfig {
+ private final @RawRangingDevice.RangingUpdateRate int mRangingUpdateRate;
+
+ SelectedCsConfig() throws ConfigSelectionException {
+ mRangingUpdateRate = selectRangingUpdateRate();
+ }
+
+ public @NonNull ImmutableSet<CsConfig> getLocalConfigs() {
+ return mPeerAddresses.entrySet().stream()
+ .map((entry) -> new CsConfig(
+ DEVICE_ROLE_INITIATOR,
+ new BleCsRangingParams.Builder(entry.getValue())
+ .setRangingUpdateRate(mRangingUpdateRate)
+ .setSecurityLevel(mPeerSecurityLevels.get(entry.getKey()))
+ .build(),
+ mSessionConfig,
+ entry.getKey()))
+ .collect(ImmutableSet.toImmutableSet());
+ }
+
+ public @NonNull ImmutableMap<RangingDevice, CsOobConfig> getPeerConfigs() {
+ CsOobConfig config = CsOobConfig.builder().build();
+ return mPeerAddresses.keySet().stream()
+ .collect(ImmutableMap.toImmutableMap(Function.identity(), (unused) -> config));
+ }
+ }
+
+ private @RawRangingDevice.RangingUpdateRate int selectRangingUpdateRate()
+ throws ConfigSelectionException {
+
+ return getDurationFromUpdateRate(mOobConfig.getRangingIntervalRange())
+ .orElseThrow(() -> new ConfigSelectionException(
+ "Configured ranging interval range is incompatible with BLE CS"));
+ }
+
+
+ private static Optional<@RawRangingDevice.RangingUpdateRate Integer> getDurationFromUpdateRate(
+ Range<Duration> configuredRates
+ ) {
+ if (configuredRates.contains(CS_UPDATE_RATE_DURATIONS.get(UPDATE_RATE_FREQUENT))) {
+ return Optional.of(UPDATE_RATE_FREQUENT);
+ } else if (configuredRates.contains(CS_UPDATE_RATE_DURATIONS.get(UPDATE_RATE_NORMAL))) {
+ return Optional.of(UPDATE_RATE_NORMAL);
+ } else if (configuredRates.contains(CS_UPDATE_RATE_DURATIONS.get(UPDATE_RATE_INFREQUENT))) {
+ return Optional.of(UPDATE_RATE_INFREQUENT);
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/ranging/service/java/com/android/server/ranging/cs/CsOobCapabilities.java b/ranging/service/java/com/android/server/ranging/cs/CsOobCapabilities.java
index 7c3c685..4099f54 100644
--- a/ranging/service/java/com/android/server/ranging/cs/CsOobCapabilities.java
+++ b/ranging/service/java/com/android/server/ranging/cs/CsOobCapabilities.java
@@ -16,6 +16,8 @@
package com.android.server.ranging.cs;
+import android.ranging.ble.cs.BleCsRangingCapabilities;
+
import com.android.server.ranging.RangingTechnology;
import com.android.server.ranging.RangingUtils.Conversions;
import com.android.server.ranging.cs.CsOobConfig.CsSecurityType;
@@ -121,6 +123,17 @@
return byteBuffer.array();
}
+ public static CsOobCapabilities fromRangingCapabilities(
+ BleCsRangingCapabilities capabilities, String address
+ ) {
+ return CsOobCapabilities.builder()
+ .setBluetoothAddress(address)
+ .setSupportedSecurityTypes(capabilities.getSupportedSecurityLevels().stream()
+ .map(CsSecurityType.SECURITY_TYPES::get)
+ .collect(ImmutableList.toImmutableList()))
+ .build();
+ }
+
/** Returns the security type for CS. */
public abstract ImmutableList<CsSecurityType> getSupportedSecurityTypes();
diff --git a/ranging/service/java/com/android/server/ranging/cs/CsOobConfig.java b/ranging/service/java/com/android/server/ranging/cs/CsOobConfig.java
index 487596d..eafa7a5 100644
--- a/ranging/service/java/com/android/server/ranging/cs/CsOobConfig.java
+++ b/ranging/service/java/com/android/server/ranging/cs/CsOobConfig.java
@@ -20,6 +20,7 @@
import com.android.server.ranging.oob.TechnologyHeader;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
@@ -42,6 +43,9 @@
LEVEL_THREE(3),
LEVEL_FOUR(4);
+ public static final ImmutableList<CsSecurityType> SECURITY_TYPES =
+ ImmutableList.copyOf(CsSecurityType.values());
+
private final int mValue;
CsSecurityType(int value) {
diff --git a/ranging/service/java/com/android/server/ranging/oob/SetConfigurationMessage.java b/ranging/service/java/com/android/server/ranging/oob/SetConfigurationMessage.java
index 2c5f70f..ac09ee7 100644
--- a/ranging/service/java/com/android/server/ranging/oob/SetConfigurationMessage.java
+++ b/ranging/service/java/com/android/server/ranging/oob/SetConfigurationMessage.java
@@ -17,6 +17,7 @@
package com.android.server.ranging.oob;
import com.android.server.ranging.RangingTechnology;
+import com.android.server.ranging.cs.CsOobConfig;
import com.android.server.ranging.uwb.UwbOobConfig;
import com.google.auto.value.AutoValue;
@@ -73,6 +74,7 @@
// Parse Configs for ranging technologies that are set
UwbOobConfig uwbConfig = null;
+ CsOobConfig csConfig = null;
int countTechsParsed = 0;
while (parseCursor < payload.length && countTechsParsed++ < rangingTechnologiesSet.size()) {
byte[] remainingBytes = Arrays.copyOfRange(payload, parseCursor, payload.length);
@@ -87,6 +89,15 @@
uwbConfig = UwbOobConfig.parseBytes(remainingBytes);
parseCursor += uwbConfig.getSize();
break;
+ case CS:
+ if (csConfig != null) {
+ throw new IllegalArgumentException(
+ "Failed to parse SetConfigurationMessage, CsConfig already set. "
+ + "Bytes: " + Arrays.toString(payload));
+ }
+ csConfig = CsOobConfig.parseBytes(remainingBytes);
+ parseCursor += csConfig.getSize();
+ break;
default:
parseCursor += techHeader.getSize();
}
@@ -134,6 +145,10 @@
@Nullable
public abstract UwbOobConfig getUwbConfig();
+ /** Returns @Nullable CsConfig data that should be used to configure CS ranging session. */
+ @Nullable
+ public abstract CsOobConfig getCsConfig();
+
/** Returns a builder for {@link SetConfigurationMessage}. */
public static Builder builder() {
return new AutoValue_SetConfigurationMessage.Builder()
@@ -156,6 +171,8 @@
public abstract Builder setUwbConfig(@Nullable UwbOobConfig uwbConfig);
+ public abstract Builder setCsConfig(@Nullable CsOobConfig csConfig);
+
abstract SetConfigurationMessage autoBuild();
public SetConfigurationMessage build() {
@@ -171,6 +188,12 @@
.contains(RangingTechnology.UWB)
== (setConfigurationMessage.getUwbConfig() != null),
"UwbConfig or rangingTechnologiesSet for UWB not set properly.");
+ Preconditions.checkArgument(
+ setConfigurationMessage
+ .getRangingTechnologiesSet()
+ .contains(RangingTechnology.CS)
+ == (setConfigurationMessage.getCsConfig() != null),
+ "csConfig or rangingTechnologiesSet for CS not set properly.");
return setConfigurationMessage;
}
}
diff --git a/ranging/service/java/com/android/server/ranging/session/OobResponderRangingSession.java b/ranging/service/java/com/android/server/ranging/session/OobResponderRangingSession.java
index 26697ec..6d33fc0 100644
--- a/ranging/service/java/com/android/server/ranging/session/OobResponderRangingSession.java
+++ b/ranging/service/java/com/android/server/ranging/session/OobResponderRangingSession.java
@@ -128,7 +128,7 @@
if (uwbCapabilities != null) {
supportedTechnologies.add(RangingTechnology.UWB);
response.setUwbCapabilities(
- UwbOobCapabilities.fromUwbCapabilities(uwbCapabilities, mMyUwbAddress));
+ UwbOobCapabilities.fromRangingCapabilities(uwbCapabilities, mMyUwbAddress));
}
}
// TODO: Other technologies
diff --git a/ranging/service/java/com/android/server/ranging/uwb/UwbConfigSelector.java b/ranging/service/java/com/android/server/ranging/uwb/UwbConfigSelector.java
index e937341..c1727c7 100644
--- a/ranging/service/java/com/android/server/ranging/uwb/UwbConfigSelector.java
+++ b/ranging/service/java/com/android/server/ranging/uwb/UwbConfigSelector.java
@@ -171,6 +171,10 @@
}
}
+ public boolean hasPeersToConfigure() {
+ return !mPeerAddresses.isEmpty();
+ }
+
public @NonNull SelectedUwbConfig selectConfig() throws ConfigSelectionException {
return new SelectedUwbConfig();
}
diff --git a/ranging/service/java/com/android/server/ranging/uwb/UwbOobCapabilities.java b/ranging/service/java/com/android/server/ranging/uwb/UwbOobCapabilities.java
index 99bb2b3..d974297 100644
--- a/ranging/service/java/com/android/server/ranging/uwb/UwbOobCapabilities.java
+++ b/ranging/service/java/com/android/server/ranging/uwb/UwbOobCapabilities.java
@@ -188,7 +188,7 @@
return byteBuffer.array();
}
- public static UwbOobCapabilities fromUwbCapabilities(
+ public static UwbOobCapabilities fromRangingCapabilities(
UwbRangingCapabilities capabilities, UwbAddress address
) {
return UwbOobCapabilities.builder()