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.feature.Flags; 26 import android.car.media.CarAudioZoneConfigInfo; 27 import android.car.media.CarVolumeGroupEvent; 28 import android.car.media.CarVolumeGroupInfo; 29 import android.car.oem.CarAudioFadeConfiguration; 30 import android.media.AudioAttributes; 31 import android.media.AudioDeviceAttributes; 32 import android.media.AudioDeviceInfo; 33 import android.util.ArrayMap; 34 import android.util.ArraySet; 35 import android.util.SparseArray; 36 import android.util.SparseIntArray; 37 import android.util.proto.ProtoOutputStream; 38 39 import com.android.car.CarLog; 40 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneConfigProto; 41 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneProto; 42 import com.android.car.audio.hal.HalAudioDeviceInfo; 43 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 44 import com.android.car.internal.util.IndentingPrintWriter; 45 import com.android.internal.annotations.GuardedBy; 46 import com.android.internal.util.Preconditions; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 54 /** 55 * A class encapsulates the configuration of an audio zone in car. 56 * 57 * An audio zone config can contain multiple {@link CarVolumeGroup}s. 58 * 59 * See also the unified car_audio_configuration.xml 60 */ 61 final class CarAudioZoneConfig { 62 63 private static final int INVALID_GROUP_ID = -1; 64 private static final int INVALID_EVENT_TYPE = 0; 65 private final int mZoneId; 66 private final int mZoneConfigId; 67 private final String mName; 68 private final boolean mIsDefault; 69 private final List<CarVolumeGroup> mVolumeGroups; 70 private final List<String> mGroupIdToNames; 71 private final Map<String, Integer> mDeviceAddressToGroupId; 72 private final CarAudioFadeConfiguration mDefaultCarAudioFadeConfiguration; 73 private final Map<AudioAttributes, 74 CarAudioFadeConfiguration> mAudioAttributesToCarAudioFadeConfiguration; 75 private final boolean mIsFadeManagerConfigurationEnabled; 76 77 private final Object mLock = new Object(); 78 79 @GuardedBy("mLock") 80 private boolean mIsSelected; 81 CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, List<String> groupIdToNames, boolean isFadeManagerConfigEnabled, CarAudioFadeConfiguration defaultCarAudioFadeConfiguration, Map<AudioAttributes, CarAudioFadeConfiguration> attrToCarAudioFadeConfiguration)82 private CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, 83 List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, 84 List<String> groupIdToNames, boolean isFadeManagerConfigEnabled, 85 CarAudioFadeConfiguration defaultCarAudioFadeConfiguration, 86 Map<AudioAttributes, CarAudioFadeConfiguration> attrToCarAudioFadeConfiguration) { 87 mName = name; 88 mZoneId = zoneId; 89 mZoneConfigId = zoneConfigId; 90 mIsDefault = isDefault; 91 mVolumeGroups = volumeGroups; 92 mDeviceAddressToGroupId = deviceAddressToGroupId; 93 mGroupIdToNames = groupIdToNames; 94 mIsSelected = false; 95 mIsFadeManagerConfigurationEnabled = isFadeManagerConfigEnabled; 96 mDefaultCarAudioFadeConfiguration = defaultCarAudioFadeConfiguration; 97 mAudioAttributesToCarAudioFadeConfiguration = attrToCarAudioFadeConfiguration; 98 } 99 getZoneId()100 int getZoneId() { 101 return mZoneId; 102 } 103 getZoneConfigId()104 int getZoneConfigId() { 105 return mZoneConfigId; 106 } 107 getName()108 String getName() { 109 return mName; 110 } 111 isDefault()112 boolean isDefault() { 113 return mIsDefault; 114 } 115 isSelected()116 boolean isSelected() { 117 synchronized (mLock) { 118 return mIsSelected; 119 } 120 } 121 setIsSelected(boolean isSelected)122 void setIsSelected(boolean isSelected) { 123 synchronized (mLock) { 124 mIsSelected = isSelected; 125 } 126 } 127 128 @Nullable getVolumeGroup(String groupName)129 CarVolumeGroup getVolumeGroup(String groupName) { 130 int groupId = mGroupIdToNames.indexOf(groupName); 131 if (groupId < 0) { 132 return null; 133 } 134 return getVolumeGroup(groupId); 135 } 136 getVolumeGroup(int groupId)137 CarVolumeGroup getVolumeGroup(int groupId) { 138 Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1, 139 "groupId(" + groupId + ") is out of range"); 140 return mVolumeGroups.get(groupId); 141 } 142 143 /** 144 * @return Snapshot of available {@link AudioDeviceAttributes}s in List. 145 */ getAudioDevice()146 List<AudioDeviceAttributes> getAudioDevice() { 147 final List<AudioDeviceAttributes> devices = new ArrayList<>(); 148 for (int index = 0; index < mVolumeGroups.size(); index++) { 149 CarVolumeGroup group = mVolumeGroups.get(index); 150 List<String> addresses = group.getAddresses(); 151 for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) { 152 devices.add(group.getCarAudioDeviceInfoForAddress(addresses.get(addressIndex)) 153 .getAudioDevice()); 154 } 155 } 156 return devices; 157 } 158 getAudioDeviceSupportingDynamicMix()159 List<AudioDeviceAttributes> getAudioDeviceSupportingDynamicMix() { 160 List<AudioDeviceAttributes> devices = new ArrayList<>(); 161 for (int index = 0; index < mVolumeGroups.size(); index++) { 162 CarVolumeGroup group = mVolumeGroups.get(index); 163 List<String> addresses = group.getAddresses(); 164 for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) { 165 String address = addresses.get(addressIndex); 166 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address); 167 if (info.canBeRoutedWithDynamicPolicyMix()) { 168 devices.add(info.getAudioDevice()); 169 } 170 } 171 } 172 return devices; 173 } 174 getVolumeGroupCount()175 int getVolumeGroupCount() { 176 return mVolumeGroups.size(); 177 } 178 179 /** 180 * @return Snapshot of available {@link CarVolumeGroup}s in array. 181 */ getVolumeGroups()182 CarVolumeGroup[] getVolumeGroups() { 183 return mVolumeGroups.toArray(new CarVolumeGroup[0]); 184 } 185 186 /** 187 * Constraints applied here for checking usage of Dynamic Mixes for routing: 188 * 189 * - One context with same AudioAttributes usage shall not be routed to 2 different devices 190 * (Dynamic Mixes supports only match on usage, not on other AudioAttributes fields. 191 * 192 * - One address shall not appear in 2 groups. CarAudioService cannot establish Dynamic Routing 193 * rules that address multiple groups. 194 */ validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)195 boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) { 196 ArraySet<String> addresses = new ArraySet<>(); 197 SparseArray<CarAudioDeviceInfo> usageToDevice = new SparseArray<>(); 198 for (int index = 0; index < mVolumeGroups.size(); index++) { 199 CarVolumeGroup group = mVolumeGroups.get(index); 200 201 List<String> groupAddresses = group.getAddresses(); 202 // Due to AudioPolicy Dynamic Mixing limitation, rules can be made only on usage and 203 // not on audio attributes. 204 // When using product strategies, AudioPolicy may not simply route on usage match. 205 // Prevent using dynamic mixes if supporting Core Routing. 206 for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) { 207 String address = groupAddresses.get(addressIndex); 208 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address); 209 List<Integer> usagesForAddress = group.getAllSupportedUsagesForAddress(address); 210 211 if (!addresses.add(address) && !useCoreAudioRouting) { 212 Slogf.w(CarLog.TAG_AUDIO, "Address %s appears in two groups, prevents" 213 + " from using dynamic policy mixes for routing" , address); 214 return false; 215 } 216 for (int usageIndex = 0; usageIndex < usagesForAddress.size(); usageIndex++) { 217 int usage = usagesForAddress.get(usageIndex); 218 CarAudioDeviceInfo infoForAttr = usageToDevice.get(usage); 219 if (infoForAttr != null && !infoForAttr.getAddress().equals(address)) { 220 Slogf.e(CarLog.TAG_AUDIO, "Addresses %s and %s can be reached with same" 221 + " usage %s, prevent from using dynamic policy mixes.", 222 infoForAttr.getAddress(), address, 223 AudioManagerHelper.usageToXsdString(usage)); 224 if (useCoreAudioRouting) { 225 infoForAttr.resetCanBeRoutedWithDynamicPolicyMix(); 226 } else { 227 return false; 228 } 229 } else { 230 usageToDevice.put(usage, info); 231 } 232 } 233 if (useCoreAudioRouting) { 234 info.resetCanBeRoutedWithDynamicPolicyMix(); 235 } 236 } 237 } 238 return true; 239 } 240 241 /** 242 * Constraints applied here: 243 * <ul> 244 * <li>One context should not appear in two groups if not relying on Core Audio for Volume 245 * management. When using core Audio, mutual exclusive contexts may reach same devices, 246 * AudioPolicyManager will apply the corresponding gain when the context is active on the common 247 * device</li> 248 * <li>All contexts are assigned</li> 249 * <li>One device should not appear in two groups</li> 250 * <li>All gain controllers in the same group have same step value</li> 251 * <li>Device types can not repeat for multiple volume groups in a configuration, see 252 * {@link CarVolumeGroup#validateDeviceTypes(Set)} for further information. 253 * When using core audio routing, device types is not considered</li> 254 * <li>Dynamic device types can only appear alone in volume group, see 255 * {@link CarVolumeGroup#validateDeviceTypes(Set)} for further information. 256 * When using core audio routing device types is not considered</li> 257 * </ul> 258 * 259 * <p>Note that it is fine that there are devices which do not appear in any group. 260 * Those devices may be reserved for other purposes. Step value validation is done in 261 * {@link CarVolumeGroupFactory#setDeviceInfoForContext(int, CarAudioDeviceInfo)} 262 */ validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting)263 boolean validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting) { 264 ArraySet<Integer> contexts = new ArraySet<>(); 265 ArraySet<String> addresses = new ArraySet<>(); 266 ArraySet<Integer> dynamicDeviceTypesInConfig = new ArraySet<>(); 267 for (int index = 0; index < mVolumeGroups.size(); index++) { 268 CarVolumeGroup group = mVolumeGroups.get(index); 269 // One context should not appear in two groups 270 int[] groupContexts = group.getContexts(); 271 for (int groupIndex = 0; groupIndex < groupContexts.length; groupIndex++) { 272 int contextId = groupContexts[groupIndex]; 273 if (!contexts.add(contextId)) { 274 Slogf.e(CarLog.TAG_AUDIO, "Context %d appears in two groups", contextId); 275 return false; 276 } 277 } 278 // One address should not appear in two groups 279 List<String> groupAddresses = group.getAddresses(); 280 for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) { 281 String address = groupAddresses.get(addressIndex); 282 if (!addresses.add(address)) { 283 if (useCoreAudioRouting) { 284 continue; 285 } 286 Slogf.w(CarLog.TAG_AUDIO, "Address appears in two groups: " + address); 287 return false; 288 } 289 } 290 if (!useCoreAudioRouting && !group.validateDeviceTypes(dynamicDeviceTypesInConfig)) { 291 Slogf.w(CarLog.TAG_AUDIO, "Failed to validate device types for config " 292 + getName()); 293 return false; 294 } 295 } 296 297 List<Integer> allContexts = carAudioContext.getAllContextsIds(); 298 for (int index = 0; index < allContexts.size(); index++) { 299 if (!contexts.contains(allContexts.get(index))) { 300 Slogf.e(CarLog.TAG_AUDIO, "Audio context %s is not assigned to a group", 301 carAudioContext.toString(allContexts.get(index))); 302 return false; 303 } 304 } 305 306 List<Integer> contextList = new ArrayList<>(contexts); 307 // All contexts are assigned 308 if (!carAudioContext.validateAllAudioAttributesSupported(contextList)) { 309 Slogf.e(CarLog.TAG_AUDIO, "Some audio attributes are not assigned to a group"); 310 return false; 311 } 312 return true; 313 } 314 synchronizeCurrentGainIndex()315 void synchronizeCurrentGainIndex() { 316 for (int index = 0; index < mVolumeGroups.size(); index++) { 317 CarVolumeGroup group = mVolumeGroups.get(index); 318 // Synchronize the internal state 319 group.setCurrentGainIndex(group.getCurrentGainIndex()); 320 } 321 } 322 isFadeManagerConfigurationEnabled()323 boolean isFadeManagerConfigurationEnabled() { 324 return mIsFadeManagerConfigurationEnabled; 325 } 326 327 @Nullable getDefaultCarAudioFadeConfiguration()328 CarAudioFadeConfiguration getDefaultCarAudioFadeConfiguration() { 329 return mDefaultCarAudioFadeConfiguration; 330 } 331 332 @Nullable getCarAudioFadeConfigurationForAudioAttributes( AudioAttributes audioAttributes)333 CarAudioFadeConfiguration getCarAudioFadeConfigurationForAudioAttributes( 334 AudioAttributes audioAttributes) { 335 Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); 336 return mAudioAttributesToCarAudioFadeConfiguration.get(audioAttributes); 337 } 338 getAllTransientCarAudioFadeConfigurations()339 Map<AudioAttributes, CarAudioFadeConfiguration> getAllTransientCarAudioFadeConfigurations() { 340 return mAudioAttributesToCarAudioFadeConfiguration; 341 } 342 343 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)344 void dump(IndentingPrintWriter writer) { 345 writer.printf("CarAudioZoneConfig(%s:%d) of zone %d isDefault? %b\n", mName, mZoneConfigId, 346 mZoneId, mIsDefault); 347 writer.increaseIndent(); 348 writer.printf("Is active (%b)\n", isActive()); 349 writer.printf("Is selected (%b)\n", isSelected()); 350 for (int index = 0; index < mVolumeGroups.size(); index++) { 351 mVolumeGroups.get(index).dump(writer); 352 } 353 writer.printf("Is fade manager configuration enabled: %b\n", 354 isFadeManagerConfigurationEnabled()); 355 if (isFadeManagerConfigurationEnabled()) { 356 writer.printf("Default car audio fade manager config name: %s\n", 357 mDefaultCarAudioFadeConfiguration == null ? "none" 358 : mDefaultCarAudioFadeConfiguration.getName()); 359 writer.printf("Transient car audio fade manager configurations#: %d\n", 360 mAudioAttributesToCarAudioFadeConfiguration.size()); 361 writer.increaseIndent(); 362 for (Map.Entry<AudioAttributes, CarAudioFadeConfiguration> entry : 363 mAudioAttributesToCarAudioFadeConfiguration.entrySet()) { 364 writer.printf("Name: " + entry.getValue().getName() 365 + ", Audio attribute: " + entry.getKey() + "\n"); 366 } 367 writer.decreaseIndent(); 368 } 369 writer.decreaseIndent(); 370 } 371 372 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)373 void dumpProto(ProtoOutputStream proto) { 374 long zoneConfigToken = proto.start(CarAudioZoneProto.ZONE_CONFIGS); 375 proto.write(CarAudioZoneConfigProto.NAME, mName); 376 proto.write(CarAudioZoneConfigProto.ID, mZoneConfigId); 377 proto.write(CarAudioZoneConfigProto.ZONE_ID, mZoneId); 378 proto.write(CarAudioZoneConfigProto.DEFAULT, mIsDefault); 379 for (int index = 0; index < mVolumeGroups.size(); index++) { 380 mVolumeGroups.get(index).dumpProto(proto); 381 } 382 proto.write(CarAudioZoneConfigProto.IS_ACTIVE, isActive()); 383 proto.write(CarAudioZoneConfigProto.IS_SELECTED, isSelected()); 384 proto.write(CarAudioZoneConfigProto.IS_FADE_MANAGER_CONFIG_ENABLED, 385 isFadeManagerConfigurationEnabled()); 386 if (isFadeManagerConfigurationEnabled()) { 387 CarAudioProtoUtils.dumpCarAudioFadeConfigurationProto(mDefaultCarAudioFadeConfiguration, 388 CarAudioZoneConfigProto.DEFAULT_CAR_AUDIO_FADE_CONFIGURATION, proto); 389 dumpAttributeToCarAudioFadeConfigProto(proto); 390 } 391 proto.end(zoneConfigToken); 392 } 393 394 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpAttributeToCarAudioFadeConfigProto(ProtoOutputStream proto)395 private void dumpAttributeToCarAudioFadeConfigProto(ProtoOutputStream proto) { 396 for (Map.Entry<AudioAttributes, CarAudioFadeConfiguration> entry : 397 mAudioAttributesToCarAudioFadeConfiguration.entrySet()) { 398 long token = proto.start(CarAudioZoneConfigProto.ATTR_TO_CAR_AUDIO_FADE_CONFIGURATION); 399 CarAudioProtoUtils.dumpCarAudioAttributesProto(entry.getKey(), CarAudioZoneConfigProto 400 .AttrToCarAudioFadeConfiguration.ATTRIBUTES, proto); 401 CarAudioProtoUtils.dumpCarAudioFadeConfigurationProto(entry.getValue(), 402 CarAudioZoneConfigProto.AttrToCarAudioFadeConfiguration 403 .CAR_AUDIO_FADE_CONFIGURATION, proto); 404 proto.end(token); 405 } 406 } 407 408 /** 409 * Update the volume groups for the new user 410 * @param userId user id to update to 411 */ updateVolumeGroupsSettingsForUser(int userId)412 void updateVolumeGroupsSettingsForUser(int userId) { 413 for (int index = 0; index < mVolumeGroups.size(); index++) { 414 mVolumeGroups.get(index).loadVolumesSettingsForUser(userId); 415 } 416 } 417 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)418 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 419 return info != null 420 && info.getAddress() != null 421 && !info.getAddress().isEmpty() 422 && containsDeviceAddress(info.getAddress()); 423 } 424 425 @Nullable getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)426 CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) { 427 for (int i = 0; i < mVolumeGroups.size(); i++) { 428 if (mVolumeGroups.get(i).hasAudioAttributes(audioAttributes)) { 429 return mVolumeGroups.get(i); 430 } 431 } 432 return null; 433 } 434 containsDeviceAddress(String deviceAddress)435 private boolean containsDeviceAddress(String deviceAddress) { 436 return mDeviceAddressToGroupId.containsKey(deviceAddress); 437 } 438 onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)439 List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons, 440 List<CarAudioGainConfigInfo> gainInfos) { 441 // [key, value] -> [groupId, eventType] 442 SparseIntArray groupIdsToEventType = new SparseIntArray(); 443 List<Integer> extraInfos = CarAudioGainMonitor.convertReasonsToExtraInfo(halReasons); 444 445 // update volume-groups 446 for (int index = 0; index < gainInfos.size(); index++) { 447 CarAudioGainConfigInfo gainInfo = gainInfos.get(index); 448 int groupId = mDeviceAddressToGroupId.getOrDefault(gainInfo.getDeviceAddress(), 449 INVALID_GROUP_ID); 450 if (groupId == INVALID_GROUP_ID) { 451 continue; 452 } 453 454 int eventType = mVolumeGroups.get(groupId).onAudioGainChanged(halReasons, gainInfo); 455 if (eventType == INVALID_EVENT_TYPE) { 456 continue; 457 } 458 if (groupIdsToEventType.get(groupId, INVALID_GROUP_ID) != INVALID_GROUP_ID) { 459 eventType |= groupIdsToEventType.get(groupId); 460 } 461 groupIdsToEventType.put(groupId, eventType); 462 } 463 464 // generate events for updated groups 465 List<CarVolumeGroupEvent> events = new ArrayList<>(groupIdsToEventType.size()); 466 for (int index = 0; index < groupIdsToEventType.size(); index++) { 467 CarVolumeGroupEvent.Builder eventBuilder = new CarVolumeGroupEvent.Builder(List.of( 468 mVolumeGroups.get(groupIdsToEventType.keyAt(index)).getCarVolumeGroupInfo()), 469 groupIdsToEventType.valueAt(index)); 470 // ensure we have valid extra-infos 471 if (!extraInfos.isEmpty()) { 472 eventBuilder.setExtraInfos(extraInfos); 473 } 474 events.add(eventBuilder.build()); 475 } 476 return events; 477 } 478 479 /** 480 * @return The car volume infos for all the volume groups in the audio zone config 481 */ getVolumeGroupInfos()482 List<CarVolumeGroupInfo> getVolumeGroupInfos() { 483 List<CarVolumeGroupInfo> groupInfos = new ArrayList<>(mVolumeGroups.size()); 484 for (int index = 0; index < mVolumeGroups.size(); index++) { 485 groupInfos.add(mVolumeGroups.get(index).getCarVolumeGroupInfo()); 486 } 487 488 return groupInfos; 489 } 490 491 /** 492 * Returns the car audio zone config info 493 */ getCarAudioZoneConfigInfo()494 CarAudioZoneConfigInfo getCarAudioZoneConfigInfo() { 495 if (Flags.carAudioDynamicDevices()) { 496 return new CarAudioZoneConfigInfo.Builder(mName, mZoneId, mZoneConfigId) 497 .setConfigVolumeGroups(getVolumeGroupInfos()).setIsActive(isActive()) 498 .setIsSelected(isSelected()).setIsDefault(isDefault()).build(); 499 } 500 // Keep legacy code till the flags becomes permanent 501 return new CarAudioZoneConfigInfo(mName, mZoneId, mZoneConfigId); 502 } 503 isActive()504 boolean isActive() { 505 for (int c = 0; c < mVolumeGroups.size(); c++) { 506 if (mVolumeGroups.get(c).isActive()) { 507 continue; 508 } 509 return false; 510 } 511 return true; 512 } 513 514 /** 515 * For the list of {@link HalAudioDeviceInfo}, update respective {@link CarAudioDeviceInfo}. 516 * If the volume group has new gains (min/max/default/current), add a 517 * {@link CarVolumeGroupEvent} 518 */ onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)519 List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) { 520 List<CarVolumeGroupEvent> events = new ArrayList<>(); 521 ArraySet<Integer> updatedGroupIds = new ArraySet<>(); 522 523 // iterate through the incoming hal device infos and update the respective groups 524 // car audio device infos 525 for (int index = 0; index < deviceInfos.size(); index++) { 526 HalAudioDeviceInfo deviceInfo = deviceInfos.get(index); 527 int groupId = mDeviceAddressToGroupId.getOrDefault(deviceInfo.getAddress(), 528 INVALID_GROUP_ID); 529 if (groupId == INVALID_GROUP_ID) { 530 continue; 531 } 532 mVolumeGroups.get(groupId).updateAudioDeviceInfo(deviceInfo); 533 updatedGroupIds.add(groupId); 534 } 535 536 // for the updated groups, recalculate the gain stages. If new gain stage, create 537 // an event to callback 538 for (int index = 0; index < updatedGroupIds.size(); index++) { 539 CarVolumeGroup group = mVolumeGroups.get(updatedGroupIds.valueAt(index)); 540 int eventType = group.calculateNewGainStageFromDeviceInfos(); 541 if (eventType != INVALID_EVENT_TYPE) { 542 events.add(new CarVolumeGroupEvent.Builder(List.of(group.getCarVolumeGroupInfo()), 543 eventType, List.of(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM)) 544 .build()); 545 } 546 } 547 return events; 548 } 549 audioDevicesAdded(List<AudioDeviceInfo> devices)550 boolean audioDevicesAdded(List<AudioDeviceInfo> devices) { 551 Objects.requireNonNull(devices, "Audio devices can not be null"); 552 // Consider that this may change in the future when multiple devices are supported 553 // per device type. When that happens we may need a way determine where the devices 554 // should be attached. The same pattern is followed in the method called from here on 555 if (isActive()) { 556 return false; 557 } 558 boolean updated = false; 559 for (int c = 0; c < mVolumeGroups.size(); c++) { 560 if (!mVolumeGroups.get(c).audioDevicesAdded(devices)) { 561 continue; 562 } 563 updated = true; 564 } 565 return updated; 566 } 567 audioDevicesRemoved(List<AudioDeviceInfo> devices)568 boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) { 569 Objects.requireNonNull(devices, "Audio devices can not be null"); 570 boolean updated = false; 571 for (int c = 0; c < mVolumeGroups.size(); c++) { 572 if (!mVolumeGroups.get(c).audioDevicesRemoved(devices)) { 573 continue; 574 } 575 updated = true; 576 } 577 return updated; 578 } 579 updateVolumeDevices(boolean useCoreAudioRouting)580 void updateVolumeDevices(boolean useCoreAudioRouting) { 581 for (int c = 0; c < mVolumeGroups.size(); c++) { 582 mVolumeGroups.get(c).updateDevices(useCoreAudioRouting); 583 } 584 } 585 586 static final class Builder { 587 private final int mZoneId; 588 private final int mZoneConfigId; 589 private final String mName; 590 private final boolean mIsDefault; 591 private final List<CarVolumeGroup> mVolumeGroups = new ArrayList<>(); 592 private final Map<String, Integer> mDeviceAddressToGroupId = new ArrayMap<>(); 593 private final List<String> mGroupIdToNames = new ArrayList<>(); 594 private final Map<AudioAttributes, 595 CarAudioFadeConfiguration> mAudioAttributesToCarAudioFadeConfiguration = 596 new ArrayMap<>(); 597 private CarAudioFadeConfiguration mDefaultCarAudioFadeConfiguration; 598 private boolean mIsFadeManagerConfigurationEnabled; 599 Builder(String name, int zoneId, int zoneConfigId, boolean isDefault)600 Builder(String name, int zoneId, int zoneConfigId, boolean isDefault) { 601 mName = Objects.requireNonNull(name, "Car audio zone config name cannot be null"); 602 mZoneId = zoneId; 603 mZoneConfigId = zoneConfigId; 604 mIsDefault = isDefault; 605 } 606 addVolumeGroup(CarVolumeGroup volumeGroup)607 Builder addVolumeGroup(CarVolumeGroup volumeGroup) { 608 mVolumeGroups.add(volumeGroup); 609 mGroupIdToNames.add(volumeGroup.getName()); 610 addGroupAddressesToMap(volumeGroup.getAddresses(), volumeGroup.getId()); 611 return this; 612 } 613 setFadeManagerConfigurationEnabled(boolean enabled)614 Builder setFadeManagerConfigurationEnabled(boolean enabled) { 615 mIsFadeManagerConfigurationEnabled = enabled; 616 return this; 617 } 618 setDefaultCarAudioFadeConfiguration( CarAudioFadeConfiguration carAudioFadeConfiguration)619 Builder setDefaultCarAudioFadeConfiguration( 620 CarAudioFadeConfiguration carAudioFadeConfiguration) { 621 mDefaultCarAudioFadeConfiguration = Objects.requireNonNull(carAudioFadeConfiguration, 622 "Car audio fade configuration for default cannot be null"); 623 return this; 624 } 625 setCarAudioFadeConfigurationForAudioAttributes(AudioAttributes audioAttributes, CarAudioFadeConfiguration carAudioFadeConfiguration)626 Builder setCarAudioFadeConfigurationForAudioAttributes(AudioAttributes audioAttributes, 627 CarAudioFadeConfiguration carAudioFadeConfiguration) { 628 Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); 629 Objects.requireNonNull(carAudioFadeConfiguration, 630 "Car audio fade configuration for audio attributes cannot be null"); 631 mAudioAttributesToCarAudioFadeConfiguration.put(audioAttributes, 632 carAudioFadeConfiguration); 633 return this; 634 } 635 getZoneId()636 int getZoneId() { 637 return mZoneId; 638 } 639 getZoneConfigId()640 int getZoneConfigId() { 641 return mZoneConfigId; 642 } 643 build()644 CarAudioZoneConfig build() { 645 if (!mIsFadeManagerConfigurationEnabled) { 646 mDefaultCarAudioFadeConfiguration = null; 647 mAudioAttributesToCarAudioFadeConfiguration.clear(); 648 } 649 return new CarAudioZoneConfig(mName, mZoneId, mZoneConfigId, mIsDefault, mVolumeGroups, 650 mDeviceAddressToGroupId, mGroupIdToNames, mIsFadeManagerConfigurationEnabled, 651 mDefaultCarAudioFadeConfiguration, mAudioAttributesToCarAudioFadeConfiguration); 652 } 653 addGroupAddressesToMap(List<String> addresses, int groupId)654 private void addGroupAddressesToMap(List<String> addresses, int groupId) { 655 for (int index = 0; index < addresses.size(); index++) { 656 mDeviceAddressToGroupId.put(addresses.get(index), groupId); 657 } 658 } 659 } 660 } 661