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