1 /* 2 * Copyright (C) 2023 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 package com.android.car.audio; 17 18 import static android.car.media.CarVolumeGroupEvent.EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM; 19 20 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 21 22 import android.annotation.Nullable; 23 import android.car.builtin.media.AudioManagerHelper; 24 import android.car.builtin.util.Slogf; 25 import android.car.media.CarAudioZoneConfigInfo; 26 import android.car.media.CarVolumeGroupEvent; 27 import android.car.media.CarVolumeGroupInfo; 28 import android.media.AudioDeviceInfo; 29 import android.util.ArrayMap; 30 import android.util.ArraySet; 31 import android.util.SparseArray; 32 import android.util.SparseIntArray; 33 34 import com.android.car.CarLog; 35 import com.android.car.audio.hal.HalAudioDeviceInfo; 36 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 37 import com.android.car.internal.util.IndentingPrintWriter; 38 import com.android.internal.util.Preconditions; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Objects; 44 45 /** 46 * A class encapsulates the configuration of an audio zone in car. 47 * 48 * An audio zone config can contain multiple {@link CarVolumeGroup}s. 49 * 50 * See also the unified car_audio_configuration.xml 51 */ 52 final class CarAudioZoneConfig { 53 54 private static final int INVALID_GROUP_ID = -1; 55 private static final int INVALID_EVENT_TYPE = 0; 56 private final int mZoneId; 57 private final int mZoneConfigId; 58 private final String mName; 59 private final boolean mIsDefault; 60 private final List<CarVolumeGroup> mVolumeGroups; 61 private final List<String> mGroupIdToNames; 62 private final Map<String, Integer> mDeviceAddressToGroupId; 63 CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, List<String> groupIdToNames)64 private CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, 65 List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, 66 List<String> groupIdToNames) { 67 mName = name; 68 mZoneId = zoneId; 69 mZoneConfigId = zoneConfigId; 70 mIsDefault = isDefault; 71 mVolumeGroups = volumeGroups; 72 mDeviceAddressToGroupId = deviceAddressToGroupId; 73 mGroupIdToNames = groupIdToNames; 74 } 75 getZoneId()76 int getZoneId() { 77 return mZoneId; 78 } 79 getZoneConfigId()80 int getZoneConfigId() { 81 return mZoneConfigId; 82 } 83 getName()84 String getName() { 85 return mName; 86 } 87 isDefault()88 boolean isDefault() { 89 return mIsDefault; 90 } 91 92 @Nullable getVolumeGroup(String groupName)93 CarVolumeGroup getVolumeGroup(String groupName) { 94 int groupId = mGroupIdToNames.indexOf(groupName); 95 if (groupId < 0) { 96 return null; 97 } 98 return getVolumeGroup(groupId); 99 } 100 getVolumeGroup(int groupId)101 CarVolumeGroup getVolumeGroup(int groupId) { 102 Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1, 103 "groupId(" + groupId + ") is out of range"); 104 return mVolumeGroups.get(groupId); 105 } 106 107 /** 108 * @return Snapshot of available {@link AudioDeviceInfo}s in List. 109 */ getAudioDeviceInfos()110 List<AudioDeviceInfo> getAudioDeviceInfos() { 111 final List<AudioDeviceInfo> devices = new ArrayList<>(); 112 for (int index = 0; index < mVolumeGroups.size(); index++) { 113 CarVolumeGroup group = mVolumeGroups.get(index); 114 List<String> addresses = group.getAddresses(); 115 for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) { 116 devices.add(group.getCarAudioDeviceInfoForAddress(addresses.get(addressIndex)) 117 .getAudioDeviceInfo()); 118 } 119 } 120 return devices; 121 } 122 getAudioDeviceInfosSupportingDynamicMix()123 List<AudioDeviceInfo> getAudioDeviceInfosSupportingDynamicMix() { 124 List<AudioDeviceInfo> devices = new ArrayList<>(); 125 for (int index = 0; index < mVolumeGroups.size(); index++) { 126 CarVolumeGroup group = mVolumeGroups.get(index); 127 List<String> addresses = group.getAddresses(); 128 for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) { 129 String address = addresses.get(addressIndex); 130 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address); 131 if (info.canBeRoutedWithDynamicPolicyMix()) { 132 devices.add(info.getAudioDeviceInfo()); 133 } 134 } 135 } 136 return devices; 137 } 138 getVolumeGroupCount()139 int getVolumeGroupCount() { 140 return mVolumeGroups.size(); 141 } 142 143 /** 144 * @return Snapshot of available {@link CarVolumeGroup}s in array. 145 */ getVolumeGroups()146 CarVolumeGroup[] getVolumeGroups() { 147 return mVolumeGroups.toArray(new CarVolumeGroup[0]); 148 } 149 150 /** 151 * Constraints applied here for checking usage of Dynamic Mixes for routing: 152 * 153 * - One context with same AudioAttributes usage shall not be routed to 2 different devices 154 * (Dynamic Mixes supports only match on usage, not on other AudioAttributes fields. 155 * 156 * - One address shall not appear in 2 groups. CarAudioService cannot establish Dynamic Routing 157 * rules that address multiple groups. 158 */ validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)159 boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) { 160 ArraySet<String> addresses = new ArraySet<>(); 161 SparseArray<CarAudioDeviceInfo> usageToDevice = new SparseArray<>(); 162 for (int index = 0; index < mVolumeGroups.size(); index++) { 163 CarVolumeGroup group = mVolumeGroups.get(index); 164 165 List<String> groupAddresses = group.getAddresses(); 166 // Due to AudioPolicy Dynamic Mixing limitation, rules can be made only on usage and 167 // not on audio attributes. 168 // When using product strategies, AudioPolicy may not simply route on usage match. 169 // Ensure that a given usage can reach a single device address to enable dynamic mix. 170 // Otherwise, prevent from establishing rule if supporting Core Routing. 171 // Returns false if Core Routing is not supported 172 for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) { 173 String address = groupAddresses.get(addressIndex); 174 boolean canUseDynamicMixRoutingForAddress = true; 175 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address); 176 List<Integer> usagesForAddress = group.getAllSupportedUsagesForAddress(address); 177 178 if (!addresses.add(address)) { 179 if (useCoreAudioRouting) { 180 Slogf.w(CarLog.TAG_AUDIO, "Address %s appears in two groups, prevents" 181 + " from using dynamic policy mixes for routing" , address); 182 canUseDynamicMixRoutingForAddress = false; 183 } 184 } 185 for (int usageIndex = 0; usageIndex < usagesForAddress.size(); usageIndex++) { 186 int usage = usagesForAddress.get(usageIndex); 187 CarAudioDeviceInfo infoForAttr = usageToDevice.get(usage); 188 if (infoForAttr != null && !infoForAttr.getAddress().equals(address)) { 189 Slogf.e(CarLog.TAG_AUDIO, "Addresses %s and %s can be reached with same" 190 + " usage %s, prevent from using dynamic policy mixes.", 191 infoForAttr.getAddress(), address, 192 AudioManagerHelper.usageToXsdString(usage)); 193 if (useCoreAudioRouting) { 194 canUseDynamicMixRoutingForAddress = false; 195 infoForAttr.resetCanBeRoutedWithDynamicPolicyMix(); 196 } else { 197 return false; 198 } 199 } else { 200 usageToDevice.put(usage, info); 201 } 202 } 203 if (!canUseDynamicMixRoutingForAddress) { 204 info.resetCanBeRoutedWithDynamicPolicyMix(); 205 } 206 } 207 } 208 return true; 209 } 210 211 /** 212 * Constraints applied here: 213 * <ul> 214 * <li>One context should not appear in two groups if not relying on Core Audio for Volume 215 * management. When using core Audio, mutual exclusive contexts may reach same devices, 216 * AudioPolicyManager will apply the corresponding gain when the context is active on the common 217 * device</li> 218 * <li>All contexts are assigned</li> 219 * <li>One device should not appear in two groups</li> 220 * <li>All gain controllers in the same group have same step value</li> 221 * </ul> 222 * 223 * Note that it is fine that there are devices which do not appear in any group. Those devices 224 * may be reserved for other purposes. 225 * Step value validation is done in 226 * {@link CarVolumeGroup.Builder#setDeviceInfoForContext(int, CarAudioDeviceInfo)} 227 */ validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting)228 boolean validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting) { 229 ArraySet<Integer> contexts = new ArraySet<>(); 230 ArraySet<String> addresses = new ArraySet<>(); 231 for (int index = 0; index < mVolumeGroups.size(); index++) { 232 CarVolumeGroup group = mVolumeGroups.get(index); 233 // One context should not appear in two groups 234 int[] groupContexts = group.getContexts(); 235 for (int groupIndex = 0; groupIndex < groupContexts.length; groupIndex++) { 236 int contextId = groupContexts[groupIndex]; 237 if (!contexts.add(contextId)) { 238 Slogf.e(CarLog.TAG_AUDIO, "Context %d appears in two groups", contextId); 239 return false; 240 } 241 } 242 // One address should not appear in two groups 243 List<String> groupAddresses = group.getAddresses(); 244 for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) { 245 String address = groupAddresses.get(addressIndex); 246 if (!addresses.add(address)) { 247 if (useCoreAudioRouting) { 248 continue; 249 } 250 Slogf.w(CarLog.TAG_AUDIO, "Address appears in two groups: " + address); 251 return false; 252 } 253 } 254 } 255 256 List<Integer> allContexts = carAudioContext.getAllContextsIds(); 257 for (int index = 0; index < allContexts.size(); index++) { 258 if (!contexts.contains(allContexts.get(index))) { 259 Slogf.e(CarLog.TAG_AUDIO, "Audio context %s is not assigned to a group", 260 carAudioContext.toString(allContexts.get(index))); 261 return false; 262 } 263 } 264 265 List<Integer> contextList = new ArrayList<>(contexts); 266 // All contexts are assigned 267 if (!carAudioContext.validateAllAudioAttributesSupported(contextList)) { 268 Slogf.e(CarLog.TAG_AUDIO, "Some audio attributes are not assigned to a group"); 269 return false; 270 } 271 return true; 272 } 273 synchronizeCurrentGainIndex()274 void synchronizeCurrentGainIndex() { 275 for (int index = 0; index < mVolumeGroups.size(); index++) { 276 CarVolumeGroup group = mVolumeGroups.get(index); 277 // Synchronize the internal state 278 group.setCurrentGainIndex(group.getCurrentGainIndex()); 279 } 280 } 281 282 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)283 void dump(IndentingPrintWriter writer) { 284 writer.printf("CarAudioZoneConfig(%s:%d) of zone %d isDefault? %b\n", mName, mZoneConfigId, 285 mZoneId, mIsDefault); 286 writer.increaseIndent(); 287 for (int index = 0; index < mVolumeGroups.size(); index++) { 288 mVolumeGroups.get(index).dump(writer); 289 } 290 writer.decreaseIndent(); 291 } 292 293 /** 294 * Update the volume groups for the new user 295 * @param userId user id to update to 296 */ updateVolumeGroupsSettingsForUser(int userId)297 void updateVolumeGroupsSettingsForUser(int userId) { 298 for (int index = 0; index < mVolumeGroups.size(); index++) { 299 mVolumeGroups.get(index).loadVolumesSettingsForUser(userId); 300 } 301 } 302 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)303 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 304 return info != null 305 && info.getAddress() != null 306 && !info.getAddress().isEmpty() 307 && containsDeviceAddress(info.getAddress()); 308 } 309 containsDeviceAddress(String deviceAddress)310 private boolean containsDeviceAddress(String deviceAddress) { 311 return mDeviceAddressToGroupId.containsKey(deviceAddress); 312 } 313 onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)314 List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons, 315 List<CarAudioGainConfigInfo> gainInfos) { 316 // [key, value] -> [groupId, eventType] 317 SparseIntArray groupIdsToEventType = new SparseIntArray(); 318 List<Integer> extraInfos = CarAudioGainMonitor.convertReasonsToExtraInfo(halReasons); 319 320 // update volume-groups 321 for (int index = 0; index < gainInfos.size(); index++) { 322 CarAudioGainConfigInfo gainInfo = gainInfos.get(index); 323 int groupId = mDeviceAddressToGroupId.getOrDefault(gainInfo.getDeviceAddress(), 324 INVALID_GROUP_ID); 325 if (groupId == INVALID_GROUP_ID) { 326 continue; 327 } 328 329 int eventType = mVolumeGroups.get(groupId).onAudioGainChanged(halReasons, gainInfo); 330 if (eventType == INVALID_EVENT_TYPE) { 331 continue; 332 } 333 if (groupIdsToEventType.get(groupId, INVALID_GROUP_ID) != INVALID_GROUP_ID) { 334 eventType |= groupIdsToEventType.get(groupId); 335 } 336 groupIdsToEventType.put(groupId, eventType); 337 } 338 339 // generate events for updated groups 340 List<CarVolumeGroupEvent> events = new ArrayList<>(groupIdsToEventType.size()); 341 for (int index = 0; index < groupIdsToEventType.size(); index++) { 342 CarVolumeGroupEvent.Builder eventBuilder = new CarVolumeGroupEvent.Builder(List.of( 343 mVolumeGroups.get(groupIdsToEventType.keyAt(index)).getCarVolumeGroupInfo()), 344 groupIdsToEventType.valueAt(index)); 345 // ensure we have valid extra-infos 346 if (!extraInfos.isEmpty()) { 347 eventBuilder.setExtraInfos(extraInfos); 348 } 349 events.add(eventBuilder.build()); 350 } 351 return events; 352 } 353 354 /** 355 * @return The car volume infos for all the volume groups in the audio zone config 356 */ getVolumeGroupInfos()357 List<CarVolumeGroupInfo> getVolumeGroupInfos() { 358 List<CarVolumeGroupInfo> groupInfos = new ArrayList<>(mVolumeGroups.size()); 359 for (int index = 0; index < mVolumeGroups.size(); index++) { 360 groupInfos.add(mVolumeGroups.get(index).getCarVolumeGroupInfo()); 361 } 362 363 return groupInfos; 364 } 365 366 /** 367 * Returns the car audio zone config info 368 */ getCarAudioZoneConfigInfo()369 CarAudioZoneConfigInfo getCarAudioZoneConfigInfo() { 370 return new CarAudioZoneConfigInfo(mName, mZoneId, mZoneConfigId); 371 } 372 373 /** 374 * For the list of {@link HalAudioDeviceInfo}, update respective {@link CarAudioDeviceInfo}. 375 * If the volume group has new gains (min/max/default/current), add a 376 * {@link CarVolumeGroupEvent} 377 */ onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)378 List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) { 379 List<CarVolumeGroupEvent> events = new ArrayList<>(); 380 ArraySet<Integer> updatedGroupIds = new ArraySet<>(); 381 382 // iterate through the incoming hal device infos and update the respective groups 383 // car audio device infos 384 for (int index = 0; index < deviceInfos.size(); index++) { 385 HalAudioDeviceInfo deviceInfo = deviceInfos.get(index); 386 int groupId = mDeviceAddressToGroupId.getOrDefault(deviceInfo.getAddress(), 387 INVALID_GROUP_ID); 388 if (groupId == INVALID_GROUP_ID) { 389 continue; 390 } 391 mVolumeGroups.get(groupId).updateAudioDeviceInfo(deviceInfo); 392 updatedGroupIds.add(groupId); 393 } 394 395 // for the updated groups, recalculate the gain stages. If new gain stage, create 396 // an event to callback 397 for (int index = 0; index < updatedGroupIds.size(); index++) { 398 CarVolumeGroup group = mVolumeGroups.get(updatedGroupIds.valueAt(index)); 399 int eventType = group.calculateNewGainStageFromDeviceInfos(); 400 if (eventType != INVALID_EVENT_TYPE) { 401 events.add(new CarVolumeGroupEvent.Builder(List.of(group.getCarVolumeGroupInfo()), 402 eventType, List.of(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM)) 403 .build()); 404 } 405 } 406 return events; 407 } 408 409 static final class Builder { 410 private final int mZoneId; 411 private final int mZoneConfigId; 412 private final String mName; 413 private final boolean mIsDefault; 414 private final List<CarVolumeGroup> mVolumeGroups = new ArrayList<>(); 415 private final Map<String, Integer> mDeviceAddressToGroupId = new ArrayMap<>(); 416 private final List<String> mGroupIdToNames = new ArrayList<>(); 417 Builder(String name, int zoneId, int zoneConfigId, boolean isDefault)418 Builder(String name, int zoneId, int zoneConfigId, boolean isDefault) { 419 mName = Objects.requireNonNull(name, "Car audio zone config name cannot be null"); 420 mZoneId = zoneId; 421 mZoneConfigId = zoneConfigId; 422 mIsDefault = isDefault; 423 } 424 addVolumeGroup(CarVolumeGroup volumeGroup)425 Builder addVolumeGroup(CarVolumeGroup volumeGroup) { 426 mVolumeGroups.add(volumeGroup); 427 mGroupIdToNames.add(volumeGroup.getName()); 428 addGroupAddressesToMap(volumeGroup.getAddresses(), volumeGroup.getId()); 429 return this; 430 } 431 getZoneId()432 int getZoneId() { 433 return mZoneId; 434 } 435 getZoneConfigId()436 int getZoneConfigId() { 437 return mZoneConfigId; 438 } 439 build()440 CarAudioZoneConfig build() { 441 return new CarAudioZoneConfig(mName, mZoneId, mZoneConfigId, mIsDefault, mVolumeGroups, 442 mDeviceAddressToGroupId, mGroupIdToNames); 443 } 444 addGroupAddressesToMap(List<String> addresses, int groupId)445 private void addGroupAddressesToMap(List<String> addresses, int groupId) { 446 for (int index = 0; index < addresses.size(); index++) { 447 mDeviceAddressToGroupId.put(addresses.get(index), groupId); 448 } 449 } 450 } 451 } 452