1 /* 2 * Copyright (C) 2018 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 com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 19 20 import android.annotation.Nullable; 21 import android.car.builtin.util.Slogf; 22 import android.car.media.CarAudioManager; 23 import android.car.media.CarAudioZoneConfigInfo; 24 import android.car.media.CarVolumeGroupEvent; 25 import android.car.media.CarVolumeGroupInfo; 26 import android.media.AudioAttributes; 27 import android.media.AudioDeviceAttributes; 28 import android.media.AudioDeviceInfo; 29 import android.media.AudioPlaybackConfiguration; 30 import android.util.SparseArray; 31 import android.util.proto.ProtoOutputStream; 32 33 import com.android.car.CarLog; 34 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneProto; 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.annotations.GuardedBy; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Objects; 43 44 /** 45 * A class encapsulates an audio zone in car. 46 * 47 * An audio zone can contain multiple {@link CarAudioZoneConfig}s, and each zone has its own 48 * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys 49 * attached to each zone. 50 * 51 * See also the unified car_audio_configuration.xml 52 */ 53 public class CarAudioZone { 54 55 private final int mId; 56 private final String mName; 57 private final CarAudioContext mCarAudioContext; 58 private final List<AudioDeviceAttributes> mInputAudioDevice; 59 // zone configuration id to zone configuration mapping 60 // We don't protect mCarAudioZoneConfigs by a lock because it's only written at XML parsing. 61 private final SparseArray<CarAudioZoneConfig> mCarAudioZoneConfigs; 62 private final Object mLock = new Object(); 63 64 @GuardedBy("mLock") 65 private int mCurrentConfigId; 66 CarAudioZone(CarAudioContext carAudioContext, String name, int id)67 CarAudioZone(CarAudioContext carAudioContext, String name, int id) { 68 mCarAudioContext = Objects.requireNonNull(carAudioContext, 69 "Car audio context can not be null"); 70 mName = name; 71 mId = id; 72 mCurrentConfigId = 0; 73 mInputAudioDevice = new ArrayList<>(); 74 mCarAudioZoneConfigs = new SparseArray<>(); 75 } 76 getCurrentConfigId()77 private int getCurrentConfigId() { 78 synchronized (mLock) { 79 return mCurrentConfigId; 80 } 81 } 82 getId()83 int getId() { 84 return mId; 85 } 86 getName()87 String getName() { 88 return mName; 89 } 90 isPrimaryZone()91 boolean isPrimaryZone() { 92 return mId == CarAudioManager.PRIMARY_AUDIO_ZONE; 93 } 94 getCurrentCarAudioZoneConfig()95 CarAudioZoneConfig getCurrentCarAudioZoneConfig() { 96 synchronized (mLock) { 97 return mCarAudioZoneConfigs.get(mCurrentConfigId); 98 } 99 } 100 101 @Nullable getDefaultAudioZoneConfigInfo()102 CarAudioZoneConfigInfo getDefaultAudioZoneConfigInfo() { 103 for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) { 104 if (!mCarAudioZoneConfigs.valueAt(c).isDefault()) { 105 continue; 106 } 107 return mCarAudioZoneConfigs.valueAt(c).getCarAudioZoneConfigInfo(); 108 } 109 // Should not be able to get here, for fully validated configuration. 110 Slogf.wtf(CarLog.TAG_AUDIO, "Audio zone " + mId 111 + " does not have a default configuration"); 112 return null; 113 } 114 getAllCarAudioZoneConfigs()115 List<CarAudioZoneConfig> getAllCarAudioZoneConfigs() { 116 List<CarAudioZoneConfig> zoneConfigList = new ArrayList<>(mCarAudioZoneConfigs.size()); 117 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 118 zoneConfigList.add(mCarAudioZoneConfigs.valueAt(index)); 119 } 120 return zoneConfigList; 121 } 122 123 @Nullable getCurrentVolumeGroup(String groupName)124 CarVolumeGroup getCurrentVolumeGroup(String groupName) { 125 return getCurrentCarAudioZoneConfig().getVolumeGroup(groupName); 126 } 127 getCurrentVolumeGroup(int groupId)128 CarVolumeGroup getCurrentVolumeGroup(int groupId) { 129 return getCurrentCarAudioZoneConfig().getVolumeGroup(groupId); 130 } 131 132 /** 133 * @return Snapshot of available {@link AudioDeviceInfo}s in List. 134 */ getCurrentAudioDevices()135 List<AudioDeviceAttributes> getCurrentAudioDevices() { 136 return getCurrentCarAudioZoneConfig().getAudioDevice(); 137 } 138 getCurrentAudioDeviceSupportingDynamicMix()139 List<AudioDeviceAttributes> getCurrentAudioDeviceSupportingDynamicMix() { 140 return getCurrentCarAudioZoneConfig().getAudioDeviceSupportingDynamicMix(); 141 } 142 getCurrentVolumeGroupCount()143 int getCurrentVolumeGroupCount() { 144 return getCurrentCarAudioZoneConfig().getVolumeGroupCount(); 145 } 146 147 /** 148 * @return Snapshot of available {@link CarVolumeGroup}s in array. 149 */ getCurrentVolumeGroups()150 CarVolumeGroup[] getCurrentVolumeGroups() { 151 return getCurrentCarAudioZoneConfig().getVolumeGroups(); 152 } 153 validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)154 boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) { 155 return getCurrentCarAudioZoneConfig().validateCanUseDynamicMixRouting(useCoreAudioRouting); 156 } 157 158 /** 159 * Constraints applied here: 160 * 161 * <ul> 162 * <li>At least one zone configuration exists. 163 * <li>Current zone configuration exists. 164 * <li>The zone id of all zone configurations matches zone id of the zone. 165 * <li>Exactly one zone configuration is default. 166 * <li>Volume groups for each zone configuration is valid (see 167 * {@link CarAudioZoneConfig#validateVolumeGroups(CarAudioContext, boolean)}). 168 * </ul> 169 */ validateZoneConfigs(boolean useCoreAudioRouting)170 boolean validateZoneConfigs(boolean useCoreAudioRouting) { 171 if (mCarAudioZoneConfigs.size() == 0) { 172 Slogf.w(CarLog.TAG_AUDIO, "No zone configurations for zone %d", mId); 173 return false; 174 } 175 boolean isDefaultConfigFound = false; 176 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 177 CarAudioZoneConfig zoneConfig = mCarAudioZoneConfigs.valueAt(index); 178 if (zoneConfig.getZoneId() != mId) { 179 Slogf.w(CarLog.TAG_AUDIO, 180 "Zone id %d of zone configuration %d does not match zone id %d", 181 zoneConfig.getZoneId(), 182 mCarAudioZoneConfigs.keyAt(index), mId); 183 return false; 184 } 185 if (zoneConfig.isDefault()) { 186 if (isDefaultConfigFound) { 187 Slogf.w(CarLog.TAG_AUDIO, 188 "Multiple default zone configurations exist in zone %d", mId); 189 return false; 190 } 191 isDefaultConfigFound = true; 192 } 193 if (!zoneConfig.validateVolumeGroups(mCarAudioContext, useCoreAudioRouting)) { 194 return false; 195 } 196 } 197 if (!isDefaultConfigFound) { 198 Slogf.w(CarLog.TAG_AUDIO, "No default zone configuration exists in zone %d", mId); 199 return false; 200 } 201 return true; 202 } 203 isCurrentZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo)204 boolean isCurrentZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo) { 205 synchronized (mLock) { 206 return configInfoSwitchedTo.equals(mCarAudioZoneConfigs.get(mCurrentConfigId) 207 .getCarAudioZoneConfigInfo()); 208 } 209 } 210 setCurrentCarZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo)211 void setCurrentCarZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo) { 212 synchronized (mLock) { 213 if (mCurrentConfigId == configInfoSwitchedTo.getConfigId()) { 214 return; 215 } 216 CarAudioZoneConfig previousConfig = mCarAudioZoneConfigs.get(mCurrentConfigId); 217 previousConfig.setIsSelected(false); 218 mCurrentConfigId = configInfoSwitchedTo.getConfigId(); 219 CarAudioZoneConfig current = mCarAudioZoneConfigs.get(mCurrentConfigId); 220 current.setIsSelected(true); 221 current.synchronizeCurrentGainIndex(); 222 current.updateVolumeDevices(); 223 } 224 } 225 init()226 void init() { 227 int defaultConfig = -1; 228 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 229 CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index); 230 // mCurrentConfigId should be the default config, but this may change in the future 231 // The configuration could be loaded from audio settings instead 232 if (!config.isDefault()) { 233 continue; 234 } 235 defaultConfig = config.getZoneConfigId(); 236 config.setIsSelected(true); 237 config.synchronizeCurrentGainIndex(); 238 config.updateVolumeDevices(); 239 break; 240 } 241 synchronized (mLock) { 242 mCurrentConfigId = defaultConfig; 243 } 244 } 245 246 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)247 void dump(IndentingPrintWriter writer) { 248 writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId, 249 isPrimaryZone()); 250 writer.increaseIndent(); 251 writer.printf("Current Config Id: %d\n", getCurrentConfigId()); 252 writer.printf("Input Audio Device Addresses\n"); 253 writer.increaseIndent(); 254 for (int index = 0; index < mInputAudioDevice.size(); index++) { 255 writer.printf("Device Address(%s)\n", mInputAudioDevice.get(index).getAddress()); 256 } 257 writer.decreaseIndent(); 258 writer.println(); 259 writer.printf("Audio Zone Configurations[%d]\n", mCarAudioZoneConfigs.size()); 260 writer.increaseIndent(); 261 for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) { 262 mCarAudioZoneConfigs.valueAt(i).dump(writer); 263 } 264 writer.decreaseIndent(); 265 writer.println(); 266 writer.decreaseIndent(); 267 } 268 269 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)270 void dumpProto(ProtoOutputStream proto) { 271 long carAudioZonesToken = proto.start(CarAudioDumpProto.CAR_AUDIO_ZONES); 272 proto.write(CarAudioZoneProto.NAME, mName); 273 proto.write(CarAudioZoneProto.ID, mId); 274 proto.write(CarAudioZoneProto.PRIMARY_ZONE, isPrimaryZone()); 275 proto.write(CarAudioZoneProto.CURRENT_ZONE_CONFIG_ID, getCurrentConfigId()); 276 for (int index = 0; index < mInputAudioDevice.size(); index++) { 277 proto.write(CarAudioZoneProto.INPUT_AUDIO_DEVICE_ADDRESSES, 278 mInputAudioDevice.get(index).getAddress()); 279 } 280 for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) { 281 mCarAudioZoneConfigs.valueAt(i).dumpProto(proto); 282 } 283 proto.end(carAudioZonesToken); 284 } 285 286 /** 287 * Return the audio device address mapping to a car audio context 288 */ getAddressForContext(int audioContext)289 public String getAddressForContext(int audioContext) { 290 mCarAudioContext.preconditionCheckAudioContext(audioContext); 291 String deviceAddress = null; 292 for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) { 293 deviceAddress = volumeGroup.getAddressForContext(audioContext); 294 if (deviceAddress != null) { 295 return deviceAddress; 296 } 297 } 298 // This should not happen unless something went wrong. 299 // Device address are unique per zone and all contexts are assigned in a zone. 300 throw new IllegalStateException("Could not find output device in zone " + mId 301 + " for audio context " + audioContext); 302 } 303 getAudioDeviceForContext(int audioContext)304 AudioDeviceAttributes getAudioDeviceForContext(int audioContext) { 305 mCarAudioContext.preconditionCheckAudioContext(audioContext); 306 for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) { 307 AudioDeviceAttributes audioDeviceAttributes = 308 volumeGroup.getAudioDeviceForContext(audioContext); 309 if (audioDeviceAttributes != null) { 310 return audioDeviceAttributes; 311 } 312 } 313 // This should not happen unless something went wrong. 314 // Device address are unique per zone and all contexts are assigned in a zone. 315 throw new IllegalStateException("Could not find output device in zone " + mId 316 + " for audio context " + audioContext); 317 } 318 319 /** 320 * Update the volume groups for the new user 321 * @param userId user id to update to 322 */ updateVolumeGroupsSettingsForUser(int userId)323 public void updateVolumeGroupsSettingsForUser(int userId) { 324 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 325 CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index); 326 if (!config.isSelected()) { 327 continue; 328 } 329 config.updateVolumeGroupsSettingsForUser(userId); 330 break; 331 } 332 } 333 addInputAudioDevice(AudioDeviceAttributes device)334 void addInputAudioDevice(AudioDeviceAttributes device) { 335 mInputAudioDevice.add(device); 336 } 337 getInputAudioDevices()338 List<AudioDeviceAttributes> getInputAudioDevices() { 339 return mInputAudioDevice; 340 } 341 addZoneConfig(CarAudioZoneConfig zoneConfig)342 void addZoneConfig(CarAudioZoneConfig zoneConfig) { 343 mCarAudioZoneConfigs.put(zoneConfig.getZoneConfigId(), zoneConfig); 344 if (zoneConfig.isDefault()) { 345 synchronized (mLock) { 346 mCurrentConfigId = zoneConfig.getZoneConfigId(); 347 } 348 } 349 } 350 findActiveAudioAttributesFromPlaybackConfigurations( List<AudioPlaybackConfiguration> configurations)351 public List<AudioAttributes> findActiveAudioAttributesFromPlaybackConfigurations( 352 List<AudioPlaybackConfiguration> configurations) { 353 Objects.requireNonNull(configurations, "Audio playback configurations can not be null"); 354 List<AudioAttributes> audioAttributes = new ArrayList<>(); 355 for (int index = 0; index < configurations.size(); index++) { 356 AudioPlaybackConfiguration configuration = configurations.get(index); 357 if (configuration.isActive()) { 358 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 359 // Note that address's context and the context actually supplied could be 360 // different 361 audioAttributes.add(configuration.getAudioAttributes()); 362 } 363 } 364 } 365 return audioAttributes; 366 } 367 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)368 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 369 return getCurrentCarAudioZoneConfig().isAudioDeviceInfoValidForZone(info); 370 } 371 372 @Nullable getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)373 CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) { 374 return getCurrentCarAudioZoneConfig().getVolumeGroupForAudioAttributes(audioAttributes); 375 } 376 onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)377 List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons, 378 List<CarAudioGainConfigInfo> gainInfos) { 379 List<CarVolumeGroupEvent> events = new ArrayList<>(); 380 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 381 List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index) 382 .onAudioGainChanged(halReasons, gainInfos); 383 // use events for callback only if current zone configuration 384 if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) { 385 events.addAll(eventsForZoneConfig); 386 } 387 } 388 return events; 389 } 390 onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)391 List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) { 392 List<CarVolumeGroupEvent> events = new ArrayList<>(); 393 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 394 List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index) 395 .onAudioPortsChanged(deviceInfos); 396 // Use events for callback only if current zone configuration 397 if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) { 398 events.addAll(eventsForZoneConfig); 399 } 400 } 401 return events; 402 } 403 404 /** 405 * Returns the car audio context set for the car audio zone 406 */ getCarAudioContext()407 public CarAudioContext getCarAudioContext() { 408 return mCarAudioContext; 409 } 410 411 /** 412 * Returns the car volume infos for all the volume groups in the audio zone 413 */ getCurrentVolumeGroupInfos()414 List<CarVolumeGroupInfo> getCurrentVolumeGroupInfos() { 415 return getCurrentCarAudioZoneConfig().getVolumeGroupInfos(); 416 } 417 418 /** 419 * Returns all audio zone config info in the audio zone 420 */ getCarAudioZoneConfigInfos()421 List<CarAudioZoneConfigInfo> getCarAudioZoneConfigInfos() { 422 List<CarAudioZoneConfigInfo> zoneConfigInfos = new ArrayList<>(mCarAudioZoneConfigs.size()); 423 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 424 zoneConfigInfos.add(mCarAudioZoneConfigs.valueAt(index).getCarAudioZoneConfigInfo()); 425 } 426 427 return zoneConfigInfos; 428 } 429 audioDevicesAdded(List<AudioDeviceInfo> devices)430 boolean audioDevicesAdded(List<AudioDeviceInfo> devices) { 431 Objects.requireNonNull(devices, "Audio devices can not be null"); 432 boolean updated = false; 433 for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) { 434 if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesAdded(devices)) { 435 continue; 436 } 437 updated = true; 438 } 439 return updated; 440 } 441 audioDevicesRemoved(List<AudioDeviceInfo> devices)442 boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) { 443 Objects.requireNonNull(devices, "Audio devices can not be null"); 444 boolean updated = false; 445 for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) { 446 if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesRemoved(devices)) { 447 continue; 448 } 449 updated = true; 450 } 451 return updated; 452 } 453 } 454