1 /* 2 * Copyright (C) 2024 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.car.audio; 18 19 import static com.android.car.audio.AudioControlZoneConverterUtils.convertAudioDevicePort; 20 import static com.android.car.audio.AudioControlZoneConverterUtils.convertAudioFadeConfiguration; 21 import static com.android.car.audio.AudioControlZoneConverterUtils.convertCarAudioContext; 22 import static com.android.car.audio.AudioControlZoneConverterUtils.convertTransientFadeConfiguration; 23 import static com.android.car.audio.AudioControlZoneConverterUtils.convertVolumeActivationConfig; 24 import static com.android.car.audio.AudioControlZoneConverterUtils.convertVolumeGroupConfig; 25 import static com.android.car.audio.AudioControlZoneConverterUtils.verifyVolumeGroupName; 26 import static com.android.car.audio.CarAudioUtils.generateAddressToCarAudioDeviceInfoMap; 27 import static com.android.car.audio.CarAudioUtils.generateAddressToInputAudioDeviceInfoMap; 28 import static com.android.car.audio.CarAudioUtils.generateCarAudioDeviceInfos; 29 30 import android.annotation.Nullable; 31 import android.car.builtin.util.Slogf; 32 import android.hardware.automotive.audiocontrol.AudioDeviceConfiguration; 33 import android.hardware.automotive.audiocontrol.AudioZone; 34 import android.hardware.automotive.audiocontrol.AudioZoneConfig; 35 import android.hardware.automotive.audiocontrol.AudioZoneFadeConfiguration; 36 import android.hardware.automotive.audiocontrol.TransientFadeConfigurationEntry; 37 import android.hardware.automotive.audiocontrol.VolumeGroupConfig; 38 import android.media.AudioDeviceAttributes; 39 import android.media.AudioDeviceInfo; 40 import android.media.AudioManager; 41 import android.media.audio.common.AudioPort; 42 import android.media.audio.common.AudioPortExt; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 46 import com.android.car.internal.util.LocalLog; 47 import com.android.internal.util.Preconditions; 48 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.List; 52 import java.util.Objects; 53 54 /** 55 * Class to convert audio control zone to car audio service zone. 56 */ 57 final class AudioControlZoneConverter { 58 59 private static final String TAG = AudioControlZoneConverter.class.getSimpleName(); 60 61 private final LocalLog mCarServiceLocalLog; 62 private final AudioManagerWrapper mAudioManager; 63 private final CarAudioSettings mCarAudioSettings; 64 private final ArrayMap<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo; 65 private final ArrayMap<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfo; 66 private final boolean mUseFadeManagerConfiguration; 67 AudioControlZoneConverter(AudioManagerWrapper audioManager, CarAudioSettings settings, LocalLog serviceLog, boolean useFadeManagerConfiguration)68 AudioControlZoneConverter(AudioManagerWrapper audioManager, CarAudioSettings settings, 69 LocalLog serviceLog, boolean useFadeManagerConfiguration) { 70 mAudioManager = Objects.requireNonNull(audioManager, "Audio manager can no be null"); 71 mCarAudioSettings = Objects.requireNonNull(settings, "Car audio settings can not be null"); 72 mCarServiceLocalLog = Objects.requireNonNull(serviceLog, 73 "Local car service logs can not be null"); 74 var carAudioDevices = generateCarAudioDeviceInfos(mAudioManager); 75 mAddressToCarAudioDeviceInfo = generateAddressToCarAudioDeviceInfoMap(carAudioDevices); 76 var audiInputDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); 77 mAddressToInputAudioDeviceInfo = generateAddressToInputAudioDeviceInfoMap(audiInputDevices); 78 mUseFadeManagerConfiguration = useFadeManagerConfiguration; 79 } 80 81 @Nullable convertAudioZone(AudioZone zone, AudioDeviceConfiguration deviceConfiguration)82 CarAudioZone convertAudioZone(AudioZone zone, AudioDeviceConfiguration deviceConfiguration) { 83 Objects.requireNonNull(zone, "Audio zone can not be null"); 84 Objects.requireNonNull(deviceConfiguration, "Audio device configuration can not be null"); 85 Objects.requireNonNull(zone.audioZoneContext, "Audio zone context can not be null"); 86 Objects.requireNonNull(zone.audioZoneContext.audioContextInfos, 87 "Audio zone context infos can not be null"); 88 Preconditions.checkArgument(!zone.audioZoneContext.audioContextInfos.isEmpty(), 89 "Audio zone context infos can not be empty"); 90 var carAudioContext = convertCarAudioContext(zone.audioZoneContext, deviceConfiguration); 91 if (carAudioContext == null || carAudioContext.getContextsInfo() == null 92 || carAudioContext.getContextsInfo().isEmpty()) { 93 String message = "Could not parse audio control HAL context"; 94 Slogf.e(TAG, message); 95 mCarServiceLocalLog.log(message); 96 return null; 97 } 98 var contextInfos = carAudioContext.getContextsInfo(); 99 var contextNameToId = new ArrayMap<String, Integer>(contextInfos.size()); 100 for (int index = 0; index < contextInfos.size(); index++) { 101 CarAudioContextInfo info = carAudioContext.getContextsInfo().get(index); 102 contextNameToId.put(info.getName(), info.getId()); 103 } 104 var carAudioZone = new CarAudioZone(carAudioContext, zone.name, zone.id); 105 int nextConfigId = 0; 106 for (int c = 0; c < zone.audioZoneConfigs.size(); c++) { 107 var config = zone.audioZoneConfigs.get(c); 108 var builder = new CarAudioZoneConfig.Builder(config.name, zone.id, nextConfigId, 109 config.isDefault); 110 if (!convertAudioZoneConfig(builder, config, carAudioContext, deviceConfiguration, 111 contextNameToId)) { 112 String message = "Failed to parse configuration " + config.name + " in zone " 113 + zone.id + ", exiting audio control HAL configuration"; 114 Slogf.e(TAG, message); 115 mCarServiceLocalLog.log(message); 116 return null; 117 } 118 carAudioZone.addZoneConfig(builder.build()); 119 nextConfigId++; 120 } 121 var conversionMessage = convertAudioInputDevices(carAudioZone, zone.inputAudioDevices); 122 if (!TextUtils.isEmpty(conversionMessage)) { 123 String message = "Failed to parse input device, conversion error message: " 124 + conversionMessage; 125 Slogf.e(TAG, message); 126 mCarServiceLocalLog.log(message); 127 return null; 128 } 129 return carAudioZone; 130 } 131 convertZonesMirroringAudioPorts(List<AudioPort> mirroringPorts)132 List<CarAudioDeviceInfo> convertZonesMirroringAudioPorts(List<AudioPort> mirroringPorts) { 133 if (mirroringPorts == null) { 134 return Collections.EMPTY_LIST; 135 } 136 var mirroringDevices = new ArrayList<CarAudioDeviceInfo>(); 137 for (int c = 0; c < mirroringPorts.size(); c++) { 138 var port = mirroringPorts.get(c); 139 var info = convertAudioDevicePort(port, mAudioManager, mAddressToCarAudioDeviceInfo); 140 if (info != null) { 141 mirroringDevices.add(info); 142 continue; 143 } 144 String message = "Could not convert mirroring devices with audio port " + port; 145 Slogf.e(TAG, message); 146 mCarServiceLocalLog.log(message); 147 return Collections.EMPTY_LIST; 148 } 149 return mirroringDevices; 150 } 151 convertAudioInputDevices(CarAudioZone carZone, List<AudioPort> inputDevices)152 private String convertAudioInputDevices(CarAudioZone carZone, List<AudioPort> inputDevices) { 153 if (inputDevices == null || inputDevices.isEmpty()) { 154 return ""; 155 } 156 for (int c = 0; c < inputDevices.size(); c++) { 157 String address = getAudioPortAddress(inputDevices.get(c)); 158 if (address == null || address.isEmpty()) { 159 return "Found empty device address while converting input device in zone " 160 + carZone.getId(); 161 } 162 var inputDevice = mAddressToInputAudioDeviceInfo.get(address); 163 if (inputDevice == null) { 164 return "Could not find input device with address " + address 165 + " while converting input device in zone " + carZone.getId(); 166 } 167 carZone.addInputAudioDevice(new AudioDeviceAttributes(inputDevice)); 168 } 169 return ""; 170 } 171 getAudioPortAddress(AudioPort audioPort)172 private String getAudioPortAddress(AudioPort audioPort) { 173 if (isInvalidInputDevice(audioPort)) { 174 return ""; 175 } 176 var device = audioPort.ext.getDevice(); 177 if (device.device == null || device.device.address == null) { 178 return ""; 179 } 180 return device.device.address.getId(); 181 } 182 isInvalidInputDevice(AudioPort port)183 private static boolean isInvalidInputDevice(AudioPort port) { 184 return port == null || port.ext == null || port.ext.getTag() != AudioPortExt.device; 185 } 186 convertAudioZoneConfig(CarAudioZoneConfig.Builder builder, AudioZoneConfig config, CarAudioContext carAudioContext, AudioDeviceConfiguration deviceConfiguration, ArrayMap<String, Integer> contextNameToId)187 private boolean convertAudioZoneConfig(CarAudioZoneConfig.Builder builder, 188 AudioZoneConfig config, CarAudioContext carAudioContext, 189 AudioDeviceConfiguration deviceConfiguration, 190 ArrayMap<String, Integer> contextNameToId) { 191 for (int c = 0; c < config.volumeGroups.size(); c++) { 192 var groupConfig = config.volumeGroups.get(c); 193 if (convertVolumeGroup(builder, groupConfig, carAudioContext, deviceConfiguration, 194 contextNameToId, c)) { 195 continue; 196 } 197 String message = "Failed to parse volume group " + groupConfig.name + " with id " 198 + groupConfig.id + " in audio zone config " + config.name; 199 Slogf.e(TAG, message); 200 mCarServiceLocalLog.log(message); 201 return false; 202 } 203 if (!convertAudioZoneFadeConfiguration(builder, config.fadeConfiguration)) { 204 return false; 205 } 206 Slogf.i(TAG, "Successfully converted audio zone config %s in zone %s", 207 builder.getZoneConfigId(), builder.getZoneId()); 208 return true; 209 } 210 convertAudioZoneFadeConfiguration(CarAudioZoneConfig.Builder builder, AudioZoneFadeConfiguration fadeConfiguration)211 private boolean convertAudioZoneFadeConfiguration(CarAudioZoneConfig.Builder builder, 212 AudioZoneFadeConfiguration fadeConfiguration) { 213 if (!mUseFadeManagerConfiguration || fadeConfiguration == null) { 214 return true; 215 } 216 if (fadeConfiguration.defaultConfiguration == null) { 217 String message = "Failed to parse default fade configuration in zone config " 218 + builder.getZoneConfigId() + " in zone " + builder.getZoneId(); 219 Slogf.e(TAG, message); 220 mCarServiceLocalLog.log(message); 221 return false; 222 } 223 var defaultConfig = convertAudioFadeConfiguration(fadeConfiguration.defaultConfiguration); 224 builder.setDefaultCarAudioFadeConfiguration(defaultConfig); 225 var transientFadeConfigs = fadeConfiguration.transientConfiguration; 226 if (transientFadeConfigs == null || transientFadeConfigs.isEmpty()) { 227 return true; 228 } 229 for (int c = 0; c < transientFadeConfigs.size(); c++) { 230 if (convertTransientFadeConfigurationEntry(builder, transientFadeConfigs.get(c))) { 231 continue; 232 } 233 return false; 234 } 235 builder.setFadeManagerConfigurationEnabled(mUseFadeManagerConfiguration); 236 return true; 237 } 238 convertTransientFadeConfigurationEntry(CarAudioZoneConfig.Builder builder, TransientFadeConfigurationEntry transientFadeConfig)239 private boolean convertTransientFadeConfigurationEntry(CarAudioZoneConfig.Builder builder, 240 TransientFadeConfigurationEntry transientFadeConfig) { 241 if (isInvalidTransientFadeConfig(transientFadeConfig)) { 242 String message = "Failed to parse transient fade configuration entry in zone" 243 + " config " + builder.getZoneConfigId() + " in zone " + builder.getZoneId(); 244 Slogf.e(TAG, message); 245 mCarServiceLocalLog.log(message); 246 return false; 247 } 248 var convertedTransientConfig = convertTransientFadeConfiguration(transientFadeConfig); 249 for (int i = 0; i < convertedTransientConfig.audioAttributes().size(); i++) { 250 var audioAttribute = convertedTransientConfig.audioAttributes().get(i); 251 builder.setCarAudioFadeConfigurationForAudioAttributes(audioAttribute, 252 convertedTransientConfig.getCarAudioFadeConfiguration()); 253 } 254 return true; 255 } 256 isInvalidTransientFadeConfig(TransientFadeConfigurationEntry config)257 private static boolean isInvalidTransientFadeConfig(TransientFadeConfigurationEntry config) { 258 return config == null || config.transientUsages == null 259 || config.transientUsages.length == 0 || config.transientFadeConfiguration == null; 260 } 261 convertVolumeGroup(CarAudioZoneConfig.Builder builder, VolumeGroupConfig volumeConfig, CarAudioContext carAudioContext, AudioDeviceConfiguration deviceConfiguration, ArrayMap<String, Integer> contextNameToId, int groupId)262 private boolean convertVolumeGroup(CarAudioZoneConfig.Builder builder, 263 VolumeGroupConfig volumeConfig, CarAudioContext carAudioContext, 264 AudioDeviceConfiguration deviceConfiguration, 265 ArrayMap<String, Integer> contextNameToId, int groupId) { 266 if (!verifyVolumeGroupName(volumeConfig.name, deviceConfiguration)) { 267 String message = "Found empty volume group name while relying on core volume for config" 268 + " id " + builder.getZoneConfigId() + " and zone id " + builder.getZoneId(); 269 Slogf.e(TAG, message); 270 mCarServiceLocalLog.log(message); 271 return false; 272 } 273 if (volumeConfig.id != VolumeGroupConfig.UNASSIGNED_ID) { 274 groupId = volumeConfig.id; 275 } 276 String volumeGroupName = volumeConfig.name != null ? volumeConfig.name : ""; 277 if (volumeGroupName.isEmpty()) { 278 volumeGroupName = "config " + builder.getZoneConfigId() + " group " + groupId; 279 } 280 281 var activationVolumeConfig = 282 convertVolumeActivationConfig(volumeConfig.activationConfiguration); 283 var factory = new CarVolumeGroupFactory(mAudioManager, mCarAudioSettings, carAudioContext, 284 builder.getZoneId(), builder.getZoneConfigId(), groupId, volumeGroupName, 285 deviceConfiguration.useCarVolumeGroupMuting, activationVolumeConfig); 286 String failureMessage = convertVolumeGroupConfig(factory, volumeConfig, mAudioManager, 287 mAddressToCarAudioDeviceInfo, contextNameToId); 288 if (!failureMessage.isEmpty()) { 289 Slogf.e(TAG, failureMessage); 290 mCarServiceLocalLog.log(failureMessage); 291 return false; 292 } 293 builder.addVolumeGroup(factory.getCarVolumeGroup(deviceConfiguration.useCoreAudioVolume)); 294 return true; 295 } 296 } 297