1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.ranging.uwb; 18 19 import static android.ranging.RangingPreference.DEVICE_ROLE_RESPONDER; 20 import static android.ranging.oob.OobInitiatorRangingConfig.SECURITY_LEVEL_BASIC; 21 import static android.ranging.oob.OobInitiatorRangingConfig.SECURITY_LEVEL_SECURE; 22 import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_FREQUENT; 23 import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_INFREQUENT; 24 import static android.ranging.raw.RawRangingDevice.UPDATE_RATE_NORMAL; 25 import static android.ranging.uwb.UwbComplexChannel.UWB_CHANNEL_5; 26 import static android.ranging.uwb.UwbComplexChannel.UWB_CHANNEL_9; 27 import static android.ranging.uwb.UwbComplexChannel.UWB_PREAMBLE_CODE_INDEX_25; 28 import static android.ranging.uwb.UwbComplexChannel.UWB_PREAMBLE_CODE_INDEX_32; 29 import static android.ranging.uwb.UwbRangingParams.CONFIG_MULTICAST_DS_TWR; 30 import static android.ranging.uwb.UwbRangingParams.CONFIG_PROVISIONED_MULTICAST_DS_TWR; 31 import static android.ranging.uwb.UwbRangingParams.CONFIG_PROVISIONED_UNICAST_DS_TWR; 32 import static android.ranging.uwb.UwbRangingParams.CONFIG_PROVISIONED_UNICAST_DS_TWR_VERY_FAST; 33 import static android.ranging.uwb.UwbRangingParams.CONFIG_UNICAST_DS_TWR; 34 35 import android.ranging.RangingDevice; 36 import android.ranging.SessionConfig; 37 import android.ranging.SessionHandle; 38 import android.ranging.oob.OobInitiatorRangingConfig; 39 import android.ranging.raw.RawRangingDevice; 40 import android.ranging.uwb.UwbAddress; 41 import android.ranging.uwb.UwbComplexChannel; 42 import android.ranging.uwb.UwbRangingCapabilities; 43 import android.ranging.uwb.UwbRangingParams; 44 import android.util.Log; 45 import android.util.Pair; 46 import android.util.Range; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 51 import com.android.ranging.uwb.backend.internal.RangingTimingParams; 52 import com.android.ranging.uwb.backend.internal.Utils; 53 import com.android.server.ranging.RangingEngine; 54 import com.android.server.ranging.RangingEngine.ConfigSelectionException; 55 import com.android.server.ranging.RangingUtils.InternalReason; 56 import com.android.server.ranging.oob.CapabilityResponseMessage; 57 import com.android.server.ranging.oob.SetConfigurationMessage.TechnologyOobConfig; 58 import com.android.server.ranging.session.RangingSessionConfig.TechnologyConfig; 59 60 import com.google.common.collect.BiMap; 61 import com.google.common.collect.HashBiMap; 62 import com.google.common.collect.ImmutableBiMap; 63 import com.google.common.collect.ImmutableMap; 64 import com.google.common.collect.ImmutableSet; 65 import com.google.common.collect.Sets; 66 67 import java.util.Collections; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Random; 71 import java.util.Set; 72 import java.util.function.Function; 73 import java.util.stream.Collectors; 74 import java.util.stream.IntStream; 75 76 /** Selects a {@link UwbConfig} from local and peer device capabilities */ 77 public class UwbConfigSelector implements RangingEngine.ConfigSelector { 78 private static final String TAG = UwbConfigSelector.class.getSimpleName(); 79 80 private static final Set<@UwbComplexChannel.UwbPreambleCodeIndex Integer> HPRF_INDEXES = 81 IntStream.rangeClosed(UWB_PREAMBLE_CODE_INDEX_25, UWB_PREAMBLE_CODE_INDEX_32) 82 .boxed() 83 .collect(Collectors.toSet()); 84 85 private final SessionConfig mSessionConfig; 86 private final OobInitiatorRangingConfig mOobConfig; 87 private final SessionHandle mSessionHandle; 88 private final BiMap<RangingDevice, UwbAddress> mPeerAddresses; 89 90 private final Set<@UwbRangingParams.ConfigId Integer> mConfigIds; 91 private final Set<@UwbComplexChannel.UwbChannel Integer> mChannels; 92 private final Set<@UwbComplexChannel.UwbPreambleCodeIndex Integer> mPreambleIndexes; 93 private @UwbRangingParams.SlotDuration int mMinSlotDurationMs; 94 private long mMinRangingIntervalMs; 95 private final String mCountryCode; 96 isCapableOfConfig( @onNull SessionConfig sessionConfig, @NonNull OobInitiatorRangingConfig oobConfig, @Nullable UwbRangingCapabilities capabilities )97 private static boolean isCapableOfConfig( 98 @NonNull SessionConfig sessionConfig, @NonNull OobInitiatorRangingConfig oobConfig, 99 @Nullable UwbRangingCapabilities capabilities 100 ) { 101 if (capabilities == null) return false; 102 103 boolean isMulticast = oobConfig.getDeviceHandles().size() > 1; 104 105 if (oobConfig.getSecurityLevel() == SECURITY_LEVEL_BASIC 106 && isMulticast 107 && !capabilities.getSupportedConfigIds().contains(CONFIG_MULTICAST_DS_TWR) 108 ) return false; 109 110 if (oobConfig.getSecurityLevel() == SECURITY_LEVEL_BASIC 111 && !isMulticast 112 && !capabilities.getSupportedConfigIds().contains(CONFIG_UNICAST_DS_TWR) 113 ) return false; 114 115 if (oobConfig.getSecurityLevel() == SECURITY_LEVEL_SECURE 116 && isMulticast 117 && !capabilities 118 .getSupportedConfigIds().contains(CONFIG_PROVISIONED_MULTICAST_DS_TWR) 119 ) return false; 120 121 if (oobConfig.getSecurityLevel() == SECURITY_LEVEL_SECURE 122 && !isMulticast 123 && !capabilities 124 .getSupportedConfigIds().contains(CONFIG_PROVISIONED_UNICAST_DS_TWR) 125 ) return false; 126 127 // TODO: If we add support for AoA via ARCore in the future, this will need to be changed. 128 if (sessionConfig.isAngleOfArrivalNeeded() && !capabilities.isAzimuthalAngleSupported()) 129 return false; 130 131 return true; 132 } 133 UwbConfigSelector( @onNull SessionConfig sessionConfig, @NonNull OobInitiatorRangingConfig oobConfig, @NonNull SessionHandle sessionHandle, @Nullable UwbRangingCapabilities capabilities )134 public UwbConfigSelector( 135 @NonNull SessionConfig sessionConfig, 136 @NonNull OobInitiatorRangingConfig oobConfig, 137 @NonNull SessionHandle sessionHandle, 138 @Nullable UwbRangingCapabilities capabilities 139 ) throws ConfigSelectionException { 140 if (!isCapableOfConfig(sessionConfig, oobConfig, capabilities)) { 141 throw new ConfigSelectionException("Local device is incapable of provided UWB config", 142 InternalReason.UNSUPPORTED); 143 } 144 145 mSessionConfig = sessionConfig; 146 mOobConfig = oobConfig; 147 mSessionHandle = sessionHandle; 148 mPeerAddresses = HashBiMap.create(); 149 mConfigIds = new HashSet<>(capabilities.getSupportedConfigIds()); 150 // TODO(406020462): Temporary debug log 151 Log.d(TAG, "Locally supported channels: " + capabilities.getSupportedChannels()); 152 mChannels = new HashSet<>(capabilities.getSupportedChannels()); 153 mPreambleIndexes = new HashSet<>(capabilities.getSupportedPreambleIndexes()); 154 mMinSlotDurationMs = Collections.min(capabilities.getSupportedSlotDurations()); 155 mMinRangingIntervalMs = capabilities.getMinimumRangingInterval().toMillis(); 156 mCountryCode = capabilities.getCountryCode(); 157 } 158 159 /** 160 * @throws ConfigSelectionException if the provided capabilities are incompatible with the 161 * configuration 162 */ 163 @Override addPeerCapabilities( @onNull RangingDevice peer, @NonNull CapabilityResponseMessage response )164 public void addPeerCapabilities( 165 @NonNull RangingDevice peer, @NonNull CapabilityResponseMessage response 166 ) throws ConfigSelectionException { 167 UwbOobCapabilities capabilities = response.getUwbCapabilities(); 168 if (capabilities == null) { 169 throw new ConfigSelectionException("Peer " + peer + " does not support UWB", 170 InternalReason.PEER_CAPABILITIES_MISMATCH); 171 } 172 if (!capabilities.getSupportedDeviceRole().contains(UwbOobConfig.OobDeviceRole.INITIATOR)) { 173 throw new ConfigSelectionException("Peer does not support initiator role", 174 InternalReason.PEER_CAPABILITIES_MISMATCH); 175 } 176 177 mPeerAddresses.put(peer, capabilities.getUwbAddress()); 178 mConfigIds.retainAll(capabilities.getSupportedConfigIds()); 179 mChannels.retainAll(capabilities.getSupportedChannels()); 180 // TODO(406020462): Temporary debug log 181 Log.d(TAG, "Add peer with supported channels " + capabilities.getSupportedChannels() 182 + " set of selectable channels updated to " + mChannels); 183 mPreambleIndexes.retainAll(capabilities.getSupportedPreambleIndexes()); 184 mMinSlotDurationMs = Math.max( 185 mMinSlotDurationMs, capabilities.getMinimumSlotDurationMs()); 186 mMinRangingIntervalMs = Math.max( 187 mMinRangingIntervalMs, capabilities.getMinimumRangingIntervalMs()); 188 } 189 190 @Override hasPeersToConfigure()191 public boolean hasPeersToConfigure() { 192 return !mPeerAddresses.isEmpty(); 193 } 194 195 @Override 196 public @NonNull Pair< 197 ImmutableSet<TechnologyConfig>, 198 ImmutableMap<RangingDevice, TechnologyOobConfig> selectConfigs()199 > selectConfigs() throws ConfigSelectionException { 200 SelectedUwbConfig configs = new SelectedUwbConfig(); 201 return Pair.create(configs.getLocalConfigs(), configs.getPeerConfigs()); 202 } 203 204 private class SelectedUwbConfig { 205 private final int mSessionId; 206 private final UwbAddress mLocalAddress; 207 private final @UwbRangingParams.ConfigId int mConfigId; 208 private final @UwbComplexChannel.UwbChannel int mChannel; 209 private final @UwbComplexChannel.UwbPreambleCodeIndex int mPreambleIndex; 210 private final @RawRangingDevice.RangingUpdateRate int mRangingUpdateRate; 211 private final byte[] mSessionKeyInfo; 212 SelectedUwbConfig()213 SelectedUwbConfig() throws ConfigSelectionException { 214 mSessionId = mSessionHandle.hashCode(); 215 mLocalAddress = UwbAddress.createRandomShortAddress(); 216 mConfigId = selectConfigId(); 217 mChannel = selectChannel(); 218 mPreambleIndex = selectPreambleIndex(); 219 mRangingUpdateRate = selectRangingUpdateRate(); 220 mSessionKeyInfo = selectSessionKeyInfo(); 221 } 222 223 // For now, each GRAPI responder will be a UWB initiator for a unicast session. In the 224 // future we can look into combining these into a single multicast session somehow. 225 getLocalConfigs()226 public @NonNull ImmutableSet<TechnologyConfig> getLocalConfigs() { 227 return mPeerAddresses.keySet().stream().map( 228 (device) -> new UwbConfig.Builder( 229 new UwbRangingParams.Builder( 230 mSessionId, mConfigId, mLocalAddress, 231 mPeerAddresses.get(device)) 232 .setSessionKeyInfo(mSessionKeyInfo) 233 .setComplexChannel(new UwbComplexChannel.Builder() 234 .setChannel(mChannel) 235 .setPreambleIndex(mPreambleIndex) 236 .build()) 237 .setRangingUpdateRate(mRangingUpdateRate) 238 .setSlotDuration(mMinSlotDurationMs) 239 .build()) 240 .setSessionConfig(mSessionConfig) 241 .setDeviceRole(DEVICE_ROLE_RESPONDER) 242 .setPeerAddresses(ImmutableBiMap.of(device, mPeerAddresses.get(device))) 243 .build()) 244 .collect(ImmutableSet.toImmutableSet()); 245 } 246 getPeerConfigs()247 public @NonNull ImmutableMap<RangingDevice, TechnologyOobConfig> getPeerConfigs() { 248 UwbOobConfig config = UwbOobConfig.builder() 249 .setUwbAddress(mLocalAddress) 250 .setSessionId(mSessionId) 251 .setSelectedConfigId(mConfigId) 252 .setSelectedChannel(mChannel) 253 .setSelectedPreambleIndex(mPreambleIndex) 254 .setSelectedRangingIntervalMs(Utils.getRangingTimingParams((int) mConfigId) 255 .getRangingInterval((int) mRangingUpdateRate)) 256 .setSelectedSlotDurationMs(mMinSlotDurationMs) 257 .setSessionKey(mSessionKeyInfo) 258 .setCountryCode(mCountryCode) 259 .setDeviceRole(UwbOobConfig.OobDeviceRole.INITIATOR) 260 .setDeviceMode(UwbOobConfig.OobDeviceMode.CONTROLLER) 261 .build(); 262 263 return mPeerAddresses.keySet().stream() 264 .collect(ImmutableMap.toImmutableMap(Function.identity(), (unused) -> config)); 265 } 266 } 267 selectConfigId()268 private @UwbRangingParams.ConfigId int selectConfigId() throws ConfigSelectionException { 269 if (mOobConfig.getSecurityLevel() == SECURITY_LEVEL_BASIC) { 270 if (mConfigIds.contains(CONFIG_UNICAST_DS_TWR)) { 271 return CONFIG_UNICAST_DS_TWR; 272 } 273 } else if (mOobConfig.getSecurityLevel() == SECURITY_LEVEL_SECURE) { 274 if (mConfigIds.contains(CONFIG_PROVISIONED_UNICAST_DS_TWR_VERY_FAST)) { 275 return CONFIG_PROVISIONED_UNICAST_DS_TWR_VERY_FAST; 276 } else if (mConfigIds.contains(CONFIG_PROVISIONED_UNICAST_DS_TWR)) { 277 return CONFIG_PROVISIONED_UNICAST_DS_TWR; 278 } 279 } 280 281 throw new ConfigSelectionException("Failed to find agreeable config id", 282 InternalReason.PEER_CAPABILITIES_MISMATCH); 283 } 284 selectSessionKeyInfo()285 private byte[] selectSessionKeyInfo() { 286 byte[] sessionKeyInfo; 287 if (mOobConfig.getSecurityLevel() == SECURITY_LEVEL_BASIC) { 288 sessionKeyInfo = new byte[8]; 289 } else { 290 sessionKeyInfo = new byte[16]; 291 } 292 new Random().nextBytes(sessionKeyInfo); 293 return sessionKeyInfo; 294 } 295 selectChannel()296 private @UwbComplexChannel.UwbChannel int selectChannel() throws ConfigSelectionException { 297 if (mChannels.contains(UWB_CHANNEL_9)) { 298 return UWB_CHANNEL_9; 299 } else if (mChannels.contains(UWB_CHANNEL_5)) { 300 return UWB_CHANNEL_5; 301 } else { 302 // TODO(b/406020462): This is a temporary workaround for the 25Q2 release. Undo this 303 // change once release snapshot is taken so we can continue to debug the failure. 304 Log.e(TAG, "Not all peers support uwb channel 9 or 5, if you see this, please attach " 305 + "logcat to b/406020462"); 306 return UWB_CHANNEL_9; 307 // throw new ConfigSelectionException("Not all peers support uwb channel 9 or 5", 308 // InternalReason.PEER_CAPABILITIES_MISMATCH); 309 } 310 } 311 selectPreambleIndex()312 private @UwbComplexChannel.UwbPreambleCodeIndex int selectPreambleIndex() 313 throws ConfigSelectionException { 314 315 if (mPreambleIndexes.isEmpty()) { 316 throw new ConfigSelectionException( 317 "Peers do not share support for any uwb preamble indexes", 318 InternalReason.PEER_CAPABILITIES_MISMATCH); 319 } 320 Set<@UwbComplexChannel.UwbPreambleCodeIndex Integer> supportedHprfIndexes = 321 Sets.intersection(mPreambleIndexes, HPRF_INDEXES); 322 323 // Prioritize HPRF indexes 324 if (!supportedHprfIndexes.isEmpty()) { 325 return List.copyOf(supportedHprfIndexes).get( 326 new Random().nextInt(supportedHprfIndexes.size())); 327 } else { 328 return List.copyOf(mPreambleIndexes).get( 329 new Random().nextInt(mPreambleIndexes.size())); 330 } 331 } 332 selectRangingUpdateRate()333 private @RawRangingDevice.RangingUpdateRate int selectRangingUpdateRate() 334 throws ConfigSelectionException { 335 336 @UwbRangingParams.ConfigId int configId = selectConfigId(); 337 RangingTimingParams timings = Utils.getRangingTimingParams((int) configId); 338 339 Range<Long> intervalsMs; 340 try { 341 intervalsMs = Range.create( 342 Math.max(mMinRangingIntervalMs, timings.getRangingIntervalFast()), 343 (long) timings.getRangingIntervalInfrequent()); 344 } catch (IllegalArgumentException unused) { 345 throw new ConfigSelectionException("Timings supported by selected config id " + configId 346 + " are incompatible with local or peer ranging interval capabilities", 347 InternalReason.PEER_CAPABILITIES_MISMATCH); 348 } 349 350 // The code below is a little hard to read, but there are 3 cases: 351 // 1. If configured range overlaps with the intervals that devices are capable of, select 352 // fastest supported. 353 // 2. If configured range lies entirely above the intervals that devices are capable of, 354 // select UPDATE_RATE_INFREQUENT. 355 // 3. If configured range lies entirely below the intervals that devices are capable of, 356 // select fastest supported. 357 try { 358 intervalsMs = intervalsMs.intersect( 359 mOobConfig.getFastestRangingInterval().toMillis(), 360 mOobConfig.getSlowestRangingInterval().toMillis()); 361 } catch (IllegalArgumentException ignored) { 362 if (mOobConfig.getFastestRangingInterval().toMillis() > intervalsMs.getUpper()) { 363 return UPDATE_RATE_INFREQUENT; 364 } 365 } 366 367 return getFastestUpdateRateInRange(intervalsMs, timings); 368 } 369 getFastestUpdateRateInRange( Range<Long> rangeMs, RangingTimingParams timings )370 private @RawRangingDevice.RangingUpdateRate int getFastestUpdateRateInRange( 371 Range<Long> rangeMs, RangingTimingParams timings 372 ) throws ConfigSelectionException { 373 if (rangeMs.contains((long) timings.getRangingIntervalFast())) { 374 return UPDATE_RATE_FREQUENT; 375 } else if (rangeMs.contains((long) timings.getRangingIntervalNormal())) { 376 return UPDATE_RATE_NORMAL; 377 } else if (rangeMs.contains((long) timings.getRangingIntervalInfrequent())) { 378 return UPDATE_RATE_INFREQUENT; 379 } else { 380 throw new ConfigSelectionException( 381 "Could not find update rate within the " 382 + "requested range that satisfies all peer capabilities", 383 InternalReason.PEER_CAPABILITIES_MISMATCH); 384 } 385 } 386 } 387