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.updateVolumeDevices(mCarAudioContext.useCoreAudioRouting()); 222 } 223 } 224 init()225 void init() { 226 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 227 CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index); 228 config.synchronizeCurrentGainIndex(); 229 // mCurrentConfigId should be the default config, but this may change in the future 230 // The configuration could be loaded from audio settings instead 231 if (!config.isDefault()) { 232 continue; 233 } 234 synchronized (mLock) { 235 mCurrentConfigId = config.getZoneConfigId(); 236 } 237 config.setIsSelected(true); 238 config.updateVolumeDevices(mCarAudioContext.useCoreAudioRouting()); 239 } 240 } 241 242 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)243 void dump(IndentingPrintWriter writer) { 244 writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId, 245 isPrimaryZone()); 246 writer.increaseIndent(); 247 writer.printf("Current Config Id: %d\n", getCurrentConfigId()); 248 writer.printf("Input Audio Device Addresses\n"); 249 writer.increaseIndent(); 250 for (int index = 0; index < mInputAudioDevice.size(); index++) { 251 writer.printf("Device Address(%s)\n", mInputAudioDevice.get(index).getAddress()); 252 } 253 writer.decreaseIndent(); 254 writer.println(); 255 writer.printf("Audio Zone Configurations[%d]\n", mCarAudioZoneConfigs.size()); 256 writer.increaseIndent(); 257 for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) { 258 mCarAudioZoneConfigs.valueAt(i).dump(writer); 259 } 260 writer.decreaseIndent(); 261 writer.println(); 262 writer.decreaseIndent(); 263 } 264 265 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)266 void dumpProto(ProtoOutputStream proto) { 267 long carAudioZonesToken = proto.start(CarAudioDumpProto.CAR_AUDIO_ZONES); 268 proto.write(CarAudioZoneProto.NAME, mName); 269 proto.write(CarAudioZoneProto.ID, mId); 270 proto.write(CarAudioZoneProto.PRIMARY_ZONE, isPrimaryZone()); 271 proto.write(CarAudioZoneProto.CURRENT_ZONE_CONFIG_ID, getCurrentConfigId()); 272 for (int index = 0; index < mInputAudioDevice.size(); index++) { 273 proto.write(CarAudioZoneProto.INPUT_AUDIO_DEVICE_ADDRESSES, 274 mInputAudioDevice.get(index).getAddress()); 275 } 276 for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) { 277 mCarAudioZoneConfigs.valueAt(i).dumpProto(proto); 278 } 279 proto.end(carAudioZonesToken); 280 } 281 282 /** 283 * Return the audio device address mapping to a car audio context 284 */ getAddressForContext(int audioContext)285 public String getAddressForContext(int audioContext) { 286 mCarAudioContext.preconditionCheckAudioContext(audioContext); 287 String deviceAddress = null; 288 for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) { 289 deviceAddress = volumeGroup.getAddressForContext(audioContext); 290 if (deviceAddress != null) { 291 return deviceAddress; 292 } 293 } 294 // This should not happen unless something went wrong. 295 // Device address are unique per zone and all contexts are assigned in a zone. 296 throw new IllegalStateException("Could not find output device in zone " + mId 297 + " for audio context " + audioContext); 298 } 299 getAudioDeviceForContext(int audioContext)300 AudioDeviceAttributes getAudioDeviceForContext(int audioContext) { 301 mCarAudioContext.preconditionCheckAudioContext(audioContext); 302 for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) { 303 AudioDeviceAttributes audioDeviceAttributes = 304 volumeGroup.getAudioDeviceForContext(audioContext); 305 if (audioDeviceAttributes != null) { 306 return audioDeviceAttributes; 307 } 308 } 309 // This should not happen unless something went wrong. 310 // Device address are unique per zone and all contexts are assigned in a zone. 311 throw new IllegalStateException("Could not find output device in zone " + mId 312 + " for audio context " + audioContext); 313 } 314 315 /** 316 * Update the volume groups for the new user 317 * @param userId user id to update to 318 */ updateVolumeGroupsSettingsForUser(int userId)319 public void updateVolumeGroupsSettingsForUser(int userId) { 320 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 321 CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index); 322 if (!config.isSelected()) { 323 continue; 324 } 325 config.updateVolumeGroupsSettingsForUser(userId); 326 break; 327 } 328 } 329 addInputAudioDevice(AudioDeviceAttributes device)330 void addInputAudioDevice(AudioDeviceAttributes device) { 331 mInputAudioDevice.add(device); 332 } 333 getInputAudioDevices()334 List<AudioDeviceAttributes> getInputAudioDevices() { 335 return mInputAudioDevice; 336 } 337 addZoneConfig(CarAudioZoneConfig zoneConfig)338 void addZoneConfig(CarAudioZoneConfig zoneConfig) { 339 mCarAudioZoneConfigs.put(zoneConfig.getZoneConfigId(), zoneConfig); 340 if (zoneConfig.isDefault()) { 341 synchronized (mLock) { 342 mCurrentConfigId = zoneConfig.getZoneConfigId(); 343 } 344 } 345 } 346 findActiveAudioAttributesFromPlaybackConfigurations( List<AudioPlaybackConfiguration> configurations)347 public List<AudioAttributes> findActiveAudioAttributesFromPlaybackConfigurations( 348 List<AudioPlaybackConfiguration> configurations) { 349 Objects.requireNonNull(configurations, "Audio playback configurations can not be null"); 350 List<AudioAttributes> audioAttributes = new ArrayList<>(); 351 for (int index = 0; index < configurations.size(); index++) { 352 AudioPlaybackConfiguration configuration = configurations.get(index); 353 if (configuration.isActive()) { 354 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 355 // Note that address's context and the context actually supplied could be 356 // different 357 audioAttributes.add(configuration.getAudioAttributes()); 358 } 359 } 360 } 361 return audioAttributes; 362 } 363 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)364 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 365 return getCurrentCarAudioZoneConfig().isAudioDeviceInfoValidForZone(info); 366 } 367 368 @Nullable getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)369 CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) { 370 return getCurrentCarAudioZoneConfig().getVolumeGroupForAudioAttributes(audioAttributes); 371 } 372 onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)373 List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons, 374 List<CarAudioGainConfigInfo> gainInfos) { 375 List<CarVolumeGroupEvent> events = new ArrayList<>(); 376 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 377 List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index) 378 .onAudioGainChanged(halReasons, gainInfos); 379 // use events for callback only if current zone configuration 380 if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) { 381 events.addAll(eventsForZoneConfig); 382 } 383 } 384 return events; 385 } 386 onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)387 List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) { 388 List<CarVolumeGroupEvent> events = new ArrayList<>(); 389 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 390 List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index) 391 .onAudioPortsChanged(deviceInfos); 392 // Use events for callback only if current zone configuration 393 if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) { 394 events.addAll(eventsForZoneConfig); 395 } 396 } 397 return events; 398 } 399 400 /** 401 * Returns the car audio context set for the car audio zone 402 */ getCarAudioContext()403 public CarAudioContext getCarAudioContext() { 404 return mCarAudioContext; 405 } 406 407 /** 408 * Returns the car volume infos for all the volume groups in the audio zone 409 */ getCurrentVolumeGroupInfos()410 List<CarVolumeGroupInfo> getCurrentVolumeGroupInfos() { 411 return getCurrentCarAudioZoneConfig().getVolumeGroupInfos(); 412 } 413 414 /** 415 * Returns all audio zone config info in the audio zone 416 */ getCarAudioZoneConfigInfos()417 List<CarAudioZoneConfigInfo> getCarAudioZoneConfigInfos() { 418 List<CarAudioZoneConfigInfo> zoneConfigInfos = new ArrayList<>(mCarAudioZoneConfigs.size()); 419 for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) { 420 zoneConfigInfos.add(mCarAudioZoneConfigs.valueAt(index).getCarAudioZoneConfigInfo()); 421 } 422 423 return zoneConfigInfos; 424 } 425 audioDevicesAdded(List<AudioDeviceInfo> devices)426 boolean audioDevicesAdded(List<AudioDeviceInfo> devices) { 427 Objects.requireNonNull(devices, "Audio devices can not be null"); 428 boolean updated = false; 429 for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) { 430 if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesAdded(devices)) { 431 continue; 432 } 433 updated = true; 434 } 435 return updated; 436 } 437 audioDevicesRemoved(List<AudioDeviceInfo> devices)438 boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) { 439 Objects.requireNonNull(devices, "Audio devices can not be null"); 440 boolean updated = false; 441 for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) { 442 if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesRemoved(devices)) { 443 continue; 444 } 445 updated = true; 446 } 447 return updated; 448 } 449 } 450