• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.media.CarAudioZoneConfigInfo;
26 import android.car.media.CarVolumeGroupEvent;
27 import android.car.media.CarVolumeGroupInfo;
28 import android.media.AudioDeviceInfo;
29 import android.util.ArrayMap;
30 import android.util.ArraySet;
31 import android.util.SparseArray;
32 import android.util.SparseIntArray;
33 
34 import com.android.car.CarLog;
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.util.Preconditions;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Objects;
44 
45 /**
46  * A class encapsulates the configuration of an audio zone in car.
47  *
48  * An audio zone config can contain multiple {@link CarVolumeGroup}s.
49  *
50  * See also the unified car_audio_configuration.xml
51  */
52 final class CarAudioZoneConfig {
53 
54     private static final int INVALID_GROUP_ID = -1;
55     private static final int INVALID_EVENT_TYPE = 0;
56     private final int mZoneId;
57     private final int mZoneConfigId;
58     private final String mName;
59     private final boolean mIsDefault;
60     private final List<CarVolumeGroup> mVolumeGroups;
61     private final List<String> mGroupIdToNames;
62     private final Map<String, Integer> mDeviceAddressToGroupId;
63 
CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, List<String> groupIdToNames)64     private CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault,
65             List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId,
66             List<String> groupIdToNames) {
67         mName = name;
68         mZoneId = zoneId;
69         mZoneConfigId = zoneConfigId;
70         mIsDefault = isDefault;
71         mVolumeGroups = volumeGroups;
72         mDeviceAddressToGroupId = deviceAddressToGroupId;
73         mGroupIdToNames = groupIdToNames;
74     }
75 
getZoneId()76     int getZoneId() {
77         return mZoneId;
78     }
79 
getZoneConfigId()80     int getZoneConfigId() {
81         return mZoneConfigId;
82     }
83 
getName()84     String getName() {
85         return mName;
86     }
87 
isDefault()88     boolean isDefault() {
89         return mIsDefault;
90     }
91 
92     @Nullable
getVolumeGroup(String groupName)93     CarVolumeGroup getVolumeGroup(String groupName) {
94         int groupId = mGroupIdToNames.indexOf(groupName);
95         if (groupId < 0) {
96             return null;
97         }
98         return getVolumeGroup(groupId);
99     }
100 
getVolumeGroup(int groupId)101     CarVolumeGroup getVolumeGroup(int groupId) {
102         Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1,
103                 "groupId(" + groupId + ") is out of range");
104         return mVolumeGroups.get(groupId);
105     }
106 
107     /**
108      * @return Snapshot of available {@link AudioDeviceInfo}s in List.
109      */
getAudioDeviceInfos()110     List<AudioDeviceInfo> getAudioDeviceInfos() {
111         final List<AudioDeviceInfo> devices = new ArrayList<>();
112         for (int index = 0; index < mVolumeGroups.size(); index++) {
113             CarVolumeGroup group = mVolumeGroups.get(index);
114             List<String> addresses = group.getAddresses();
115             for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) {
116                 devices.add(group.getCarAudioDeviceInfoForAddress(addresses.get(addressIndex))
117                         .getAudioDeviceInfo());
118             }
119         }
120         return devices;
121     }
122 
getAudioDeviceInfosSupportingDynamicMix()123     List<AudioDeviceInfo> getAudioDeviceInfosSupportingDynamicMix() {
124         List<AudioDeviceInfo> devices = new ArrayList<>();
125         for (int index = 0; index <  mVolumeGroups.size(); index++) {
126             CarVolumeGroup group = mVolumeGroups.get(index);
127             List<String> addresses = group.getAddresses();
128             for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) {
129                 String address = addresses.get(addressIndex);
130                 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address);
131                 if (info.canBeRoutedWithDynamicPolicyMix()) {
132                     devices.add(info.getAudioDeviceInfo());
133                 }
134             }
135         }
136         return devices;
137     }
138 
getVolumeGroupCount()139     int getVolumeGroupCount() {
140         return mVolumeGroups.size();
141     }
142 
143     /**
144      * @return Snapshot of available {@link CarVolumeGroup}s in array.
145      */
getVolumeGroups()146     CarVolumeGroup[] getVolumeGroups() {
147         return mVolumeGroups.toArray(new CarVolumeGroup[0]);
148     }
149 
150     /**
151      * Constraints applied here for checking usage of Dynamic Mixes for routing:
152      *
153      * - One context with same AudioAttributes usage shall not be routed to 2 different devices
154      * (Dynamic Mixes supports only match on usage, not on other AudioAttributes fields.
155      *
156      * - One address shall not appear in 2 groups. CarAudioService cannot establish Dynamic Routing
157      * rules that address multiple groups.
158      */
validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)159     boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) {
160         ArraySet<String> addresses = new ArraySet<>();
161         SparseArray<CarAudioDeviceInfo> usageToDevice = new SparseArray<>();
162         for (int index = 0; index <  mVolumeGroups.size(); index++) {
163             CarVolumeGroup group = mVolumeGroups.get(index);
164 
165             List<String> groupAddresses = group.getAddresses();
166             // Due to AudioPolicy Dynamic Mixing limitation,  rules can be made only on usage and
167             // not on audio attributes.
168             // When using product strategies, AudioPolicy may not simply route on usage match.
169             // Ensure that a given usage can reach a single device address to enable dynamic mix.
170             // Otherwise, prevent from establishing rule if supporting Core Routing.
171             // Returns false if Core Routing is not supported
172             for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) {
173                 String address = groupAddresses.get(addressIndex);
174                 boolean canUseDynamicMixRoutingForAddress = true;
175                 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address);
176                 List<Integer> usagesForAddress = group.getAllSupportedUsagesForAddress(address);
177 
178                 if (!addresses.add(address)) {
179                     if (useCoreAudioRouting) {
180                         Slogf.w(CarLog.TAG_AUDIO, "Address %s appears in two groups, prevents"
181                                 + " from using dynamic policy mixes for routing" , address);
182                         canUseDynamicMixRoutingForAddress = false;
183                     }
184                 }
185                 for (int usageIndex = 0; usageIndex < usagesForAddress.size(); usageIndex++) {
186                     int usage = usagesForAddress.get(usageIndex);
187                     CarAudioDeviceInfo infoForAttr = usageToDevice.get(usage);
188                     if (infoForAttr != null && !infoForAttr.getAddress().equals(address)) {
189                         Slogf.e(CarLog.TAG_AUDIO, "Addresses %s and %s can be reached with same"
190                                         + " usage %s, prevent from using dynamic policy mixes.",
191                                 infoForAttr.getAddress(), address,
192                                 AudioManagerHelper.usageToXsdString(usage));
193                         if (useCoreAudioRouting) {
194                             canUseDynamicMixRoutingForAddress = false;
195                             infoForAttr.resetCanBeRoutedWithDynamicPolicyMix();
196                         } else {
197                             return false;
198                         }
199                     } else {
200                         usageToDevice.put(usage, info);
201                     }
202                 }
203                 if (!canUseDynamicMixRoutingForAddress) {
204                     info.resetCanBeRoutedWithDynamicPolicyMix();
205                 }
206             }
207         }
208         return true;
209     }
210 
211     /**
212      * Constraints applied here:
213      * <ul>
214      * <li>One context should not appear in two groups if not relying on Core Audio for Volume
215      * management. When using core Audio, mutual exclusive contexts may reach same devices,
216      * AudioPolicyManager will apply the corresponding gain when the context is active on the common
217      * device</li>
218      * <li>All contexts are assigned</li>
219      * <li>One device should not appear in two groups</li>
220      * <li>All gain controllers in the same group have same step value</li>
221      * </ul>
222      *
223      * Note that it is fine that there are devices which do not appear in any group. Those devices
224      * may be reserved for other purposes.
225      * Step value validation is done in
226      * {@link CarVolumeGroup.Builder#setDeviceInfoForContext(int, CarAudioDeviceInfo)}
227      */
validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting)228     boolean validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting) {
229         ArraySet<Integer> contexts = new ArraySet<>();
230         ArraySet<String> addresses = new ArraySet<>();
231         for (int index = 0; index <  mVolumeGroups.size(); index++) {
232             CarVolumeGroup group = mVolumeGroups.get(index);
233             // One context should not appear in two groups
234             int[] groupContexts = group.getContexts();
235             for (int groupIndex = 0; groupIndex < groupContexts.length; groupIndex++) {
236                 int contextId = groupContexts[groupIndex];
237                 if (!contexts.add(contextId)) {
238                     Slogf.e(CarLog.TAG_AUDIO, "Context %d appears in two groups", contextId);
239                     return false;
240                 }
241             }
242             // One address should not appear in two groups
243             List<String> groupAddresses = group.getAddresses();
244             for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) {
245                 String address = groupAddresses.get(addressIndex);
246                 if (!addresses.add(address)) {
247                     if (useCoreAudioRouting) {
248                         continue;
249                     }
250                     Slogf.w(CarLog.TAG_AUDIO, "Address appears in two groups: " + address);
251                     return false;
252                 }
253             }
254         }
255 
256         List<Integer> allContexts = carAudioContext.getAllContextsIds();
257         for (int index = 0; index < allContexts.size(); index++) {
258             if (!contexts.contains(allContexts.get(index))) {
259                 Slogf.e(CarLog.TAG_AUDIO, "Audio context %s is not assigned to a group",
260                         carAudioContext.toString(allContexts.get(index)));
261                 return false;
262             }
263         }
264 
265         List<Integer> contextList = new ArrayList<>(contexts);
266         // All contexts are assigned
267         if (!carAudioContext.validateAllAudioAttributesSupported(contextList)) {
268             Slogf.e(CarLog.TAG_AUDIO, "Some audio attributes are not assigned to a group");
269             return false;
270         }
271         return true;
272     }
273 
synchronizeCurrentGainIndex()274     void synchronizeCurrentGainIndex() {
275         for (int index = 0; index < mVolumeGroups.size(); index++) {
276             CarVolumeGroup group = mVolumeGroups.get(index);
277             // Synchronize the internal state
278             group.setCurrentGainIndex(group.getCurrentGainIndex());
279         }
280     }
281 
282     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)283     void dump(IndentingPrintWriter writer) {
284         writer.printf("CarAudioZoneConfig(%s:%d) of zone %d isDefault? %b\n", mName, mZoneConfigId,
285                 mZoneId, mIsDefault);
286         writer.increaseIndent();
287         for (int index = 0; index < mVolumeGroups.size(); index++) {
288             mVolumeGroups.get(index).dump(writer);
289         }
290         writer.decreaseIndent();
291     }
292 
293     /**
294      * Update the volume groups for the new user
295      * @param userId user id to update to
296      */
updateVolumeGroupsSettingsForUser(int userId)297     void updateVolumeGroupsSettingsForUser(int userId) {
298         for (int index = 0; index < mVolumeGroups.size(); index++) {
299             mVolumeGroups.get(index).loadVolumesSettingsForUser(userId);
300         }
301     }
302 
isAudioDeviceInfoValidForZone(AudioDeviceInfo info)303     boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) {
304         return info != null
305                 && info.getAddress() != null
306                 && !info.getAddress().isEmpty()
307                 && containsDeviceAddress(info.getAddress());
308     }
309 
containsDeviceAddress(String deviceAddress)310     private boolean containsDeviceAddress(String deviceAddress) {
311         return mDeviceAddressToGroupId.containsKey(deviceAddress);
312     }
313 
onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)314     List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons,
315             List<CarAudioGainConfigInfo> gainInfos) {
316         // [key, value] -> [groupId, eventType]
317         SparseIntArray groupIdsToEventType = new SparseIntArray();
318         List<Integer> extraInfos = CarAudioGainMonitor.convertReasonsToExtraInfo(halReasons);
319 
320         // update volume-groups
321         for (int index = 0; index < gainInfos.size(); index++) {
322             CarAudioGainConfigInfo gainInfo = gainInfos.get(index);
323             int groupId = mDeviceAddressToGroupId.getOrDefault(gainInfo.getDeviceAddress(),
324                     INVALID_GROUP_ID);
325             if (groupId == INVALID_GROUP_ID) {
326                 continue;
327             }
328 
329             int eventType = mVolumeGroups.get(groupId).onAudioGainChanged(halReasons, gainInfo);
330             if (eventType == INVALID_EVENT_TYPE) {
331                 continue;
332             }
333             if (groupIdsToEventType.get(groupId, INVALID_GROUP_ID) != INVALID_GROUP_ID) {
334                 eventType |= groupIdsToEventType.get(groupId);
335             }
336             groupIdsToEventType.put(groupId, eventType);
337         }
338 
339         // generate events for updated groups
340         List<CarVolumeGroupEvent> events = new ArrayList<>(groupIdsToEventType.size());
341         for (int index = 0; index < groupIdsToEventType.size(); index++) {
342             CarVolumeGroupEvent.Builder eventBuilder = new CarVolumeGroupEvent.Builder(List.of(
343                     mVolumeGroups.get(groupIdsToEventType.keyAt(index)).getCarVolumeGroupInfo()),
344                     groupIdsToEventType.valueAt(index));
345             // ensure we have valid extra-infos
346             if (!extraInfos.isEmpty()) {
347                 eventBuilder.setExtraInfos(extraInfos);
348             }
349             events.add(eventBuilder.build());
350         }
351         return events;
352     }
353 
354     /**
355      * @return The car volume infos for all the volume groups in the audio zone config
356      */
getVolumeGroupInfos()357     List<CarVolumeGroupInfo> getVolumeGroupInfos() {
358         List<CarVolumeGroupInfo> groupInfos = new ArrayList<>(mVolumeGroups.size());
359         for (int index = 0; index < mVolumeGroups.size(); index++) {
360             groupInfos.add(mVolumeGroups.get(index).getCarVolumeGroupInfo());
361         }
362 
363         return groupInfos;
364     }
365 
366     /**
367      * Returns the car audio zone config info
368      */
getCarAudioZoneConfigInfo()369     CarAudioZoneConfigInfo getCarAudioZoneConfigInfo() {
370         return new CarAudioZoneConfigInfo(mName, mZoneId, mZoneConfigId);
371     }
372 
373     /**
374      * For the list of {@link HalAudioDeviceInfo}, update respective {@link CarAudioDeviceInfo}.
375      * If the volume group has new gains (min/max/default/current), add a
376      * {@link CarVolumeGroupEvent}
377      */
onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)378     List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) {
379         List<CarVolumeGroupEvent> events = new ArrayList<>();
380         ArraySet<Integer> updatedGroupIds = new ArraySet<>();
381 
382         // iterate through the incoming hal device infos and update the respective groups
383         // car audio device infos
384         for (int index = 0; index < deviceInfos.size(); index++) {
385             HalAudioDeviceInfo deviceInfo = deviceInfos.get(index);
386             int groupId = mDeviceAddressToGroupId.getOrDefault(deviceInfo.getAddress(),
387                     INVALID_GROUP_ID);
388             if (groupId == INVALID_GROUP_ID) {
389                 continue;
390             }
391             mVolumeGroups.get(groupId).updateAudioDeviceInfo(deviceInfo);
392             updatedGroupIds.add(groupId);
393         }
394 
395         // for the updated groups, recalculate the gain stages. If new gain stage, create
396         // an event to callback
397         for (int index = 0; index < updatedGroupIds.size(); index++) {
398             CarVolumeGroup group = mVolumeGroups.get(updatedGroupIds.valueAt(index));
399             int eventType = group.calculateNewGainStageFromDeviceInfos();
400             if (eventType != INVALID_EVENT_TYPE) {
401                 events.add(new CarVolumeGroupEvent.Builder(List.of(group.getCarVolumeGroupInfo()),
402                         eventType, List.of(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM))
403                         .build());
404             }
405         }
406         return events;
407     }
408 
409     static final class Builder {
410         private final int mZoneId;
411         private final int mZoneConfigId;
412         private final String mName;
413         private final boolean mIsDefault;
414         private final List<CarVolumeGroup> mVolumeGroups = new ArrayList<>();
415         private final Map<String, Integer> mDeviceAddressToGroupId = new ArrayMap<>();
416         private final List<String> mGroupIdToNames = new ArrayList<>();
417 
Builder(String name, int zoneId, int zoneConfigId, boolean isDefault)418         Builder(String name, int zoneId, int zoneConfigId, boolean isDefault) {
419             mName = Objects.requireNonNull(name, "Car audio zone config name cannot be null");
420             mZoneId = zoneId;
421             mZoneConfigId = zoneConfigId;
422             mIsDefault = isDefault;
423         }
424 
addVolumeGroup(CarVolumeGroup volumeGroup)425         Builder addVolumeGroup(CarVolumeGroup volumeGroup) {
426             mVolumeGroups.add(volumeGroup);
427             mGroupIdToNames.add(volumeGroup.getName());
428             addGroupAddressesToMap(volumeGroup.getAddresses(), volumeGroup.getId());
429             return this;
430         }
431 
getZoneId()432         int getZoneId() {
433             return mZoneId;
434         }
435 
getZoneConfigId()436         int getZoneConfigId() {
437             return mZoneConfigId;
438         }
439 
build()440         CarAudioZoneConfig build() {
441             return new CarAudioZoneConfig(mName, mZoneId, mZoneConfigId, mIsDefault, mVolumeGroups,
442                     mDeviceAddressToGroupId, mGroupIdToNames);
443         }
444 
addGroupAddressesToMap(List<String> addresses, int groupId)445         private void addGroupAddressesToMap(List<String> addresses, int groupId) {
446             for (int index = 0; index < addresses.size(); index++) {
447                 mDeviceAddressToGroupId.put(addresses.get(index), groupId);
448             }
449         }
450     }
451 }
452