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.feature.Flags;
26 import android.car.media.CarAudioZoneConfigInfo;
27 import android.car.media.CarVolumeGroupEvent;
28 import android.car.media.CarVolumeGroupInfo;
29 import android.car.oem.CarAudioFadeConfiguration;
30 import android.media.AudioAttributes;
31 import android.media.AudioDeviceAttributes;
32 import android.media.AudioDeviceInfo;
33 import android.util.ArrayMap;
34 import android.util.ArraySet;
35 import android.util.SparseArray;
36 import android.util.SparseIntArray;
37 import android.util.proto.ProtoOutputStream;
38 
39 import com.android.car.CarLog;
40 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneConfigProto;
41 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneProto;
42 import com.android.car.audio.hal.HalAudioDeviceInfo;
43 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
44 import com.android.car.internal.util.IndentingPrintWriter;
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.internal.util.Preconditions;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 /**
55  * A class encapsulates the configuration of an audio zone in car.
56  *
57  * An audio zone config can contain multiple {@link CarVolumeGroup}s.
58  *
59  * See also the unified car_audio_configuration.xml
60  */
61 final class CarAudioZoneConfig {
62 
63     private static final int INVALID_GROUP_ID = -1;
64     private static final int INVALID_EVENT_TYPE = 0;
65     private final int mZoneId;
66     private final int mZoneConfigId;
67     private final String mName;
68     private final boolean mIsDefault;
69     private final List<CarVolumeGroup> mVolumeGroups;
70     private final List<String> mGroupIdToNames;
71     private final Map<String, Integer> mDeviceAddressToGroupId;
72     private final CarAudioFadeConfiguration mDefaultCarAudioFadeConfiguration;
73     private final Map<AudioAttributes,
74             CarAudioFadeConfiguration> mAudioAttributesToCarAudioFadeConfiguration;
75     private final boolean mIsFadeManagerConfigurationEnabled;
76 
77     private final Object mLock = new Object();
78 
79     @GuardedBy("mLock")
80     private boolean mIsSelected;
81 
CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault, List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId, List<String> groupIdToNames, boolean isFadeManagerConfigEnabled, CarAudioFadeConfiguration defaultCarAudioFadeConfiguration, Map<AudioAttributes, CarAudioFadeConfiguration> attrToCarAudioFadeConfiguration)82     private CarAudioZoneConfig(String name, int zoneId, int zoneConfigId, boolean isDefault,
83             List<CarVolumeGroup> volumeGroups, Map<String, Integer> deviceAddressToGroupId,
84             List<String> groupIdToNames, boolean isFadeManagerConfigEnabled,
85             CarAudioFadeConfiguration defaultCarAudioFadeConfiguration,
86             Map<AudioAttributes, CarAudioFadeConfiguration> attrToCarAudioFadeConfiguration) {
87         mName = name;
88         mZoneId = zoneId;
89         mZoneConfigId = zoneConfigId;
90         mIsDefault = isDefault;
91         mVolumeGroups = volumeGroups;
92         mDeviceAddressToGroupId = deviceAddressToGroupId;
93         mGroupIdToNames = groupIdToNames;
94         mIsSelected = false;
95         mIsFadeManagerConfigurationEnabled = isFadeManagerConfigEnabled;
96         mDefaultCarAudioFadeConfiguration = defaultCarAudioFadeConfiguration;
97         mAudioAttributesToCarAudioFadeConfiguration = attrToCarAudioFadeConfiguration;
98     }
99 
getZoneId()100     int getZoneId() {
101         return mZoneId;
102     }
103 
getZoneConfigId()104     int getZoneConfigId() {
105         return mZoneConfigId;
106     }
107 
getName()108     String getName() {
109         return mName;
110     }
111 
isDefault()112     boolean isDefault() {
113         return mIsDefault;
114     }
115 
isSelected()116     boolean isSelected() {
117         synchronized (mLock) {
118             return mIsSelected;
119         }
120     }
121 
setIsSelected(boolean isSelected)122     void setIsSelected(boolean isSelected) {
123         synchronized (mLock) {
124             mIsSelected = isSelected;
125         }
126     }
127 
128     @Nullable
getVolumeGroup(String groupName)129     CarVolumeGroup getVolumeGroup(String groupName) {
130         int groupId = mGroupIdToNames.indexOf(groupName);
131         if (groupId < 0) {
132             return null;
133         }
134         return getVolumeGroup(groupId);
135     }
136 
getVolumeGroup(int groupId)137     CarVolumeGroup getVolumeGroup(int groupId) {
138         Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1,
139                 "groupId(" + groupId + ") is out of range");
140         return mVolumeGroups.get(groupId);
141     }
142 
143     /**
144      * @return Snapshot of available {@link AudioDeviceAttributes}s in List.
145      */
getAudioDevice()146     List<AudioDeviceAttributes> getAudioDevice() {
147         final List<AudioDeviceAttributes> devices = new ArrayList<>();
148         for (int index = 0; index < mVolumeGroups.size(); index++) {
149             CarVolumeGroup group = mVolumeGroups.get(index);
150             List<String> addresses = group.getAddresses();
151             for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) {
152                 devices.add(group.getCarAudioDeviceInfoForAddress(addresses.get(addressIndex))
153                         .getAudioDevice());
154             }
155         }
156         return devices;
157     }
158 
getAudioDeviceSupportingDynamicMix()159     List<AudioDeviceAttributes> getAudioDeviceSupportingDynamicMix() {
160         List<AudioDeviceAttributes> devices = new ArrayList<>();
161         for (int index = 0; index <  mVolumeGroups.size(); index++) {
162             CarVolumeGroup group = mVolumeGroups.get(index);
163             List<String> addresses = group.getAddresses();
164             for (int addressIndex = 0; addressIndex < addresses.size(); addressIndex++) {
165                 String address = addresses.get(addressIndex);
166                 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address);
167                 if (info.canBeRoutedWithDynamicPolicyMix()) {
168                     devices.add(info.getAudioDevice());
169                 }
170             }
171         }
172         return devices;
173     }
174 
getVolumeGroupCount()175     int getVolumeGroupCount() {
176         return mVolumeGroups.size();
177     }
178 
179     /**
180      * @return Snapshot of available {@link CarVolumeGroup}s in array.
181      */
getVolumeGroups()182     CarVolumeGroup[] getVolumeGroups() {
183         return mVolumeGroups.toArray(new CarVolumeGroup[0]);
184     }
185 
186     /**
187      * Constraints applied here for checking usage of Dynamic Mixes for routing:
188      *
189      * - One context with same AudioAttributes usage shall not be routed to 2 different devices
190      * (Dynamic Mixes supports only match on usage, not on other AudioAttributes fields.
191      *
192      * - One address shall not appear in 2 groups. CarAudioService cannot establish Dynamic Routing
193      * rules that address multiple groups.
194      */
validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)195     boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) {
196         ArraySet<String> addresses = new ArraySet<>();
197         SparseArray<CarAudioDeviceInfo> usageToDevice = new SparseArray<>();
198         for (int index = 0; index <  mVolumeGroups.size(); index++) {
199             CarVolumeGroup group = mVolumeGroups.get(index);
200 
201             List<String> groupAddresses = group.getAddresses();
202             // Due to AudioPolicy Dynamic Mixing limitation, rules can be made only on usage and
203             // not on audio attributes.
204             // When using product strategies, AudioPolicy may not simply route on usage match.
205             // Prevent using dynamic mixes if supporting Core Routing.
206             for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) {
207                 String address = groupAddresses.get(addressIndex);
208                 CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address);
209                 List<Integer> usagesForAddress = group.getAllSupportedUsagesForAddress(address);
210 
211                 if (!addresses.add(address) && !useCoreAudioRouting) {
212                     Slogf.w(CarLog.TAG_AUDIO, "Address %s appears in two groups, prevents"
213                             + " from using dynamic policy mixes for routing" , address);
214                     return false;
215                 }
216                 for (int usageIndex = 0; usageIndex < usagesForAddress.size(); usageIndex++) {
217                     int usage = usagesForAddress.get(usageIndex);
218                     CarAudioDeviceInfo infoForAttr = usageToDevice.get(usage);
219                     if (infoForAttr != null && !infoForAttr.getAddress().equals(address)) {
220                         Slogf.e(CarLog.TAG_AUDIO, "Addresses %s and %s can be reached with same"
221                                         + " usage %s, prevent from using dynamic policy mixes.",
222                                 infoForAttr.getAddress(), address,
223                                 AudioManagerHelper.usageToXsdString(usage));
224                         if (useCoreAudioRouting) {
225                             infoForAttr.resetCanBeRoutedWithDynamicPolicyMix();
226                         } else {
227                             return false;
228                         }
229                     } else {
230                         usageToDevice.put(usage, info);
231                     }
232                 }
233                 if (useCoreAudioRouting) {
234                     info.resetCanBeRoutedWithDynamicPolicyMix();
235                 }
236             }
237         }
238         return true;
239     }
240 
241     /**
242      * Constraints applied here:
243      * <ul>
244      * <li>One context should not appear in two groups if not relying on Core Audio for Volume
245      * management. When using core Audio, mutual exclusive contexts may reach same devices,
246      * AudioPolicyManager will apply the corresponding gain when the context is active on the common
247      * device</li>
248      * <li>All contexts are assigned</li>
249      * <li>One device should not appear in two groups</li>
250      * <li>All gain controllers in the same group have same step value</li>
251      * <li>Device types can not repeat for multiple volume groups in a configuration, see
252      * {@link CarVolumeGroup#validateDeviceTypes(Set)} for further information.
253      * When using core audio routing, device types is not considered</li>
254      * <li>Dynamic device types can only appear alone in volume group, see
255      * {@link CarVolumeGroup#validateDeviceTypes(Set)} for further information.
256      * When using core audio routing device types is not considered</li>
257      * </ul>
258      *
259      * <p>Note that it is fine that there are devices which do not appear in any group.
260      * Those devices may be reserved for other purposes. Step value validation is done in
261      * {@link CarVolumeGroupFactory#setDeviceInfoForContext(int, CarAudioDeviceInfo)}
262      */
validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting)263     boolean validateVolumeGroups(CarAudioContext carAudioContext, boolean useCoreAudioRouting) {
264         ArraySet<Integer> contexts = new ArraySet<>();
265         ArraySet<String> addresses = new ArraySet<>();
266         ArraySet<Integer> dynamicDeviceTypesInConfig = new ArraySet<>();
267         for (int index = 0; index <  mVolumeGroups.size(); index++) {
268             CarVolumeGroup group = mVolumeGroups.get(index);
269             // One context should not appear in two groups
270             int[] groupContexts = group.getContexts();
271             for (int groupIndex = 0; groupIndex < groupContexts.length; groupIndex++) {
272                 int contextId = groupContexts[groupIndex];
273                 if (!contexts.add(contextId)) {
274                     Slogf.e(CarLog.TAG_AUDIO, "Context %d appears in two groups", contextId);
275                     return false;
276                 }
277             }
278             // One address should not appear in two groups
279             List<String> groupAddresses = group.getAddresses();
280             for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) {
281                 String address = groupAddresses.get(addressIndex);
282                 if (!addresses.add(address)) {
283                     if (useCoreAudioRouting) {
284                         continue;
285                     }
286                     Slogf.w(CarLog.TAG_AUDIO, "Address appears in two groups: " + address);
287                     return false;
288                 }
289             }
290             if (!useCoreAudioRouting && !group.validateDeviceTypes(dynamicDeviceTypesInConfig)) {
291                 Slogf.w(CarLog.TAG_AUDIO, "Failed to validate device types for config "
292                         + getName());
293                 return false;
294             }
295         }
296 
297         List<Integer> allContexts = carAudioContext.getAllContextsIds();
298         for (int index = 0; index < allContexts.size(); index++) {
299             if (!contexts.contains(allContexts.get(index))) {
300                 Slogf.e(CarLog.TAG_AUDIO, "Audio context %s is not assigned to a group",
301                         carAudioContext.toString(allContexts.get(index)));
302                 return false;
303             }
304         }
305 
306         List<Integer> contextList = new ArrayList<>(contexts);
307         // All contexts are assigned
308         if (!carAudioContext.validateAllAudioAttributesSupported(contextList)) {
309             Slogf.e(CarLog.TAG_AUDIO, "Some audio attributes are not assigned to a group");
310             return false;
311         }
312         return true;
313     }
314 
synchronizeCurrentGainIndex()315     void synchronizeCurrentGainIndex() {
316         for (int index = 0; index < mVolumeGroups.size(); index++) {
317             CarVolumeGroup group = mVolumeGroups.get(index);
318             // Synchronize the internal state
319             group.setCurrentGainIndex(group.getCurrentGainIndex());
320         }
321     }
322 
isFadeManagerConfigurationEnabled()323     boolean isFadeManagerConfigurationEnabled() {
324         return mIsFadeManagerConfigurationEnabled;
325     }
326 
327     @Nullable
getDefaultCarAudioFadeConfiguration()328     CarAudioFadeConfiguration getDefaultCarAudioFadeConfiguration() {
329         return mDefaultCarAudioFadeConfiguration;
330     }
331 
332     @Nullable
getCarAudioFadeConfigurationForAudioAttributes( AudioAttributes audioAttributes)333     CarAudioFadeConfiguration getCarAudioFadeConfigurationForAudioAttributes(
334             AudioAttributes audioAttributes) {
335         Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null");
336         return mAudioAttributesToCarAudioFadeConfiguration.get(audioAttributes);
337     }
338 
getAllTransientCarAudioFadeConfigurations()339     Map<AudioAttributes, CarAudioFadeConfiguration> getAllTransientCarAudioFadeConfigurations() {
340         return mAudioAttributesToCarAudioFadeConfiguration;
341     }
342 
343     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)344     void dump(IndentingPrintWriter writer) {
345         writer.printf("CarAudioZoneConfig(%s:%d) of zone %d isDefault? %b\n", mName, mZoneConfigId,
346                 mZoneId, mIsDefault);
347         writer.increaseIndent();
348         writer.printf("Is active (%b)\n", isActive());
349         writer.printf("Is selected (%b)\n", isSelected());
350         for (int index = 0; index < mVolumeGroups.size(); index++) {
351             mVolumeGroups.get(index).dump(writer);
352         }
353         writer.printf("Is fade manager configuration enabled: %b\n",
354                 isFadeManagerConfigurationEnabled());
355         if (isFadeManagerConfigurationEnabled()) {
356             writer.printf("Default car audio fade manager config name: %s\n",
357                     mDefaultCarAudioFadeConfiguration == null ? "none"
358                     : mDefaultCarAudioFadeConfiguration.getName());
359             writer.printf("Transient car audio fade manager configurations#: %d\n",
360                     mAudioAttributesToCarAudioFadeConfiguration.size());
361             writer.increaseIndent();
362             for (Map.Entry<AudioAttributes, CarAudioFadeConfiguration> entry :
363                     mAudioAttributesToCarAudioFadeConfiguration.entrySet()) {
364                 writer.printf("Name: " + entry.getValue().getName()
365                         + ", Audio attribute: " + entry.getKey() + "\n");
366             }
367             writer.decreaseIndent();
368         }
369         writer.decreaseIndent();
370     }
371 
372     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)373     void dumpProto(ProtoOutputStream proto) {
374         long zoneConfigToken = proto.start(CarAudioZoneProto.ZONE_CONFIGS);
375         proto.write(CarAudioZoneConfigProto.NAME, mName);
376         proto.write(CarAudioZoneConfigProto.ID, mZoneConfigId);
377         proto.write(CarAudioZoneConfigProto.ZONE_ID, mZoneId);
378         proto.write(CarAudioZoneConfigProto.DEFAULT, mIsDefault);
379         for (int index = 0; index < mVolumeGroups.size(); index++) {
380             mVolumeGroups.get(index).dumpProto(proto);
381         }
382         proto.write(CarAudioZoneConfigProto.IS_ACTIVE, isActive());
383         proto.write(CarAudioZoneConfigProto.IS_SELECTED, isSelected());
384         proto.write(CarAudioZoneConfigProto.IS_FADE_MANAGER_CONFIG_ENABLED,
385                 isFadeManagerConfigurationEnabled());
386         if (isFadeManagerConfigurationEnabled()) {
387             CarAudioProtoUtils.dumpCarAudioFadeConfigurationProto(mDefaultCarAudioFadeConfiguration,
388                     CarAudioZoneConfigProto.DEFAULT_CAR_AUDIO_FADE_CONFIGURATION, proto);
389             dumpAttributeToCarAudioFadeConfigProto(proto);
390         }
391         proto.end(zoneConfigToken);
392     }
393 
394     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpAttributeToCarAudioFadeConfigProto(ProtoOutputStream proto)395     private void dumpAttributeToCarAudioFadeConfigProto(ProtoOutputStream proto) {
396         for (Map.Entry<AudioAttributes, CarAudioFadeConfiguration> entry :
397                 mAudioAttributesToCarAudioFadeConfiguration.entrySet()) {
398             long token = proto.start(CarAudioZoneConfigProto.ATTR_TO_CAR_AUDIO_FADE_CONFIGURATION);
399             CarAudioProtoUtils.dumpCarAudioAttributesProto(entry.getKey(), CarAudioZoneConfigProto
400                     .AttrToCarAudioFadeConfiguration.ATTRIBUTES, proto);
401             CarAudioProtoUtils.dumpCarAudioFadeConfigurationProto(entry.getValue(),
402                     CarAudioZoneConfigProto.AttrToCarAudioFadeConfiguration
403                             .CAR_AUDIO_FADE_CONFIGURATION, proto);
404             proto.end(token);
405         }
406     }
407 
408     /**
409      * Update the volume groups for the new user
410      * @param userId user id to update to
411      */
updateVolumeGroupsSettingsForUser(int userId)412     void updateVolumeGroupsSettingsForUser(int userId) {
413         for (int index = 0; index < mVolumeGroups.size(); index++) {
414             mVolumeGroups.get(index).loadVolumesSettingsForUser(userId);
415         }
416     }
417 
isAudioDeviceInfoValidForZone(AudioDeviceInfo info)418     boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) {
419         return info != null
420                 && info.getAddress() != null
421                 && !info.getAddress().isEmpty()
422                 && containsDeviceAddress(info.getAddress());
423     }
424 
425     @Nullable
getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)426     CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) {
427         for (int i = 0; i < mVolumeGroups.size(); i++) {
428             if (mVolumeGroups.get(i).hasAudioAttributes(audioAttributes)) {
429                 return mVolumeGroups.get(i);
430             }
431         }
432         return null;
433     }
434 
containsDeviceAddress(String deviceAddress)435     private boolean containsDeviceAddress(String deviceAddress) {
436         return mDeviceAddressToGroupId.containsKey(deviceAddress);
437     }
438 
onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)439     List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons,
440             List<CarAudioGainConfigInfo> gainInfos) {
441         // [key, value] -> [groupId, eventType]
442         SparseIntArray groupIdsToEventType = new SparseIntArray();
443         List<Integer> extraInfos = CarAudioGainMonitor.convertReasonsToExtraInfo(halReasons);
444 
445         // update volume-groups
446         for (int index = 0; index < gainInfos.size(); index++) {
447             CarAudioGainConfigInfo gainInfo = gainInfos.get(index);
448             int groupId = mDeviceAddressToGroupId.getOrDefault(gainInfo.getDeviceAddress(),
449                     INVALID_GROUP_ID);
450             if (groupId == INVALID_GROUP_ID) {
451                 continue;
452             }
453 
454             int eventType = mVolumeGroups.get(groupId).onAudioGainChanged(halReasons, gainInfo);
455             if (eventType == INVALID_EVENT_TYPE) {
456                 continue;
457             }
458             if (groupIdsToEventType.get(groupId, INVALID_GROUP_ID) != INVALID_GROUP_ID) {
459                 eventType |= groupIdsToEventType.get(groupId);
460             }
461             groupIdsToEventType.put(groupId, eventType);
462         }
463 
464         // generate events for updated groups
465         List<CarVolumeGroupEvent> events = new ArrayList<>(groupIdsToEventType.size());
466         for (int index = 0; index < groupIdsToEventType.size(); index++) {
467             CarVolumeGroupEvent.Builder eventBuilder = new CarVolumeGroupEvent.Builder(List.of(
468                     mVolumeGroups.get(groupIdsToEventType.keyAt(index)).getCarVolumeGroupInfo()),
469                     groupIdsToEventType.valueAt(index));
470             // ensure we have valid extra-infos
471             if (!extraInfos.isEmpty()) {
472                 eventBuilder.setExtraInfos(extraInfos);
473             }
474             events.add(eventBuilder.build());
475         }
476         return events;
477     }
478 
479     /**
480      * @return The car volume infos for all the volume groups in the audio zone config
481      */
getVolumeGroupInfos()482     List<CarVolumeGroupInfo> getVolumeGroupInfos() {
483         List<CarVolumeGroupInfo> groupInfos = new ArrayList<>(mVolumeGroups.size());
484         for (int index = 0; index < mVolumeGroups.size(); index++) {
485             groupInfos.add(mVolumeGroups.get(index).getCarVolumeGroupInfo());
486         }
487 
488         return groupInfos;
489     }
490 
491     /**
492      * Returns the car audio zone config info
493      */
getCarAudioZoneConfigInfo()494     CarAudioZoneConfigInfo getCarAudioZoneConfigInfo() {
495         if (Flags.carAudioDynamicDevices()) {
496             return new CarAudioZoneConfigInfo.Builder(mName, mZoneId, mZoneConfigId)
497                     .setConfigVolumeGroups(getVolumeGroupInfos()).setIsActive(isActive())
498                     .setIsSelected(isSelected()).setIsDefault(isDefault()).build();
499         }
500         // Keep legacy code till the flags becomes permanent
501         return new CarAudioZoneConfigInfo(mName, mZoneId, mZoneConfigId);
502     }
503 
isActive()504     boolean isActive() {
505         for (int c = 0; c < mVolumeGroups.size(); c++) {
506             if (mVolumeGroups.get(c).isActive()) {
507                 continue;
508             }
509             return false;
510         }
511         return true;
512     }
513 
514     /**
515      * For the list of {@link HalAudioDeviceInfo}, update respective {@link CarAudioDeviceInfo}.
516      * If the volume group has new gains (min/max/default/current), add a
517      * {@link CarVolumeGroupEvent}
518      */
onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)519     List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) {
520         List<CarVolumeGroupEvent> events = new ArrayList<>();
521         ArraySet<Integer> updatedGroupIds = new ArraySet<>();
522 
523         // iterate through the incoming hal device infos and update the respective groups
524         // car audio device infos
525         for (int index = 0; index < deviceInfos.size(); index++) {
526             HalAudioDeviceInfo deviceInfo = deviceInfos.get(index);
527             int groupId = mDeviceAddressToGroupId.getOrDefault(deviceInfo.getAddress(),
528                     INVALID_GROUP_ID);
529             if (groupId == INVALID_GROUP_ID) {
530                 continue;
531             }
532             mVolumeGroups.get(groupId).updateAudioDeviceInfo(deviceInfo);
533             updatedGroupIds.add(groupId);
534         }
535 
536         // for the updated groups, recalculate the gain stages. If new gain stage, create
537         // an event to callback
538         for (int index = 0; index < updatedGroupIds.size(); index++) {
539             CarVolumeGroup group = mVolumeGroups.get(updatedGroupIds.valueAt(index));
540             int eventType = group.calculateNewGainStageFromDeviceInfos();
541             if (eventType != INVALID_EVENT_TYPE) {
542                 events.add(new CarVolumeGroupEvent.Builder(List.of(group.getCarVolumeGroupInfo()),
543                         eventType, List.of(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM))
544                         .build());
545             }
546         }
547         return events;
548     }
549 
audioDevicesAdded(List<AudioDeviceInfo> devices)550     boolean audioDevicesAdded(List<AudioDeviceInfo> devices) {
551         Objects.requireNonNull(devices, "Audio devices can not be null");
552         // Consider that this may change in the future when multiple devices are supported
553         // per device type. When that happens we may need a way determine where the devices
554         // should be attached. The same pattern is followed in the method called from here on
555         if (isActive()) {
556             return false;
557         }
558         boolean updated = false;
559         for (int c = 0; c < mVolumeGroups.size(); c++) {
560             if (!mVolumeGroups.get(c).audioDevicesAdded(devices)) {
561                 continue;
562             }
563             updated = true;
564         }
565         return updated;
566     }
567 
audioDevicesRemoved(List<AudioDeviceInfo> devices)568     boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) {
569         Objects.requireNonNull(devices, "Audio devices can not be null");
570         boolean updated = false;
571         for (int c = 0; c < mVolumeGroups.size(); c++) {
572             if (!mVolumeGroups.get(c).audioDevicesRemoved(devices)) {
573                 continue;
574             }
575             updated = true;
576         }
577         return updated;
578     }
579 
updateVolumeDevices(boolean useCoreAudioRouting)580     void updateVolumeDevices(boolean useCoreAudioRouting) {
581         for (int c = 0; c < mVolumeGroups.size(); c++) {
582             mVolumeGroups.get(c).updateDevices(useCoreAudioRouting);
583         }
584     }
585 
586     static final class Builder {
587         private final int mZoneId;
588         private final int mZoneConfigId;
589         private final String mName;
590         private final boolean mIsDefault;
591         private final List<CarVolumeGroup> mVolumeGroups = new ArrayList<>();
592         private final Map<String, Integer> mDeviceAddressToGroupId = new ArrayMap<>();
593         private final List<String> mGroupIdToNames = new ArrayList<>();
594         private final Map<AudioAttributes,
595                 CarAudioFadeConfiguration> mAudioAttributesToCarAudioFadeConfiguration =
596                 new ArrayMap<>();
597         private CarAudioFadeConfiguration mDefaultCarAudioFadeConfiguration;
598         private boolean mIsFadeManagerConfigurationEnabled;
599 
Builder(String name, int zoneId, int zoneConfigId, boolean isDefault)600         Builder(String name, int zoneId, int zoneConfigId, boolean isDefault) {
601             mName = Objects.requireNonNull(name, "Car audio zone config name cannot be null");
602             mZoneId = zoneId;
603             mZoneConfigId = zoneConfigId;
604             mIsDefault = isDefault;
605         }
606 
addVolumeGroup(CarVolumeGroup volumeGroup)607         Builder addVolumeGroup(CarVolumeGroup volumeGroup) {
608             mVolumeGroups.add(volumeGroup);
609             mGroupIdToNames.add(volumeGroup.getName());
610             addGroupAddressesToMap(volumeGroup.getAddresses(), volumeGroup.getId());
611             return this;
612         }
613 
setFadeManagerConfigurationEnabled(boolean enabled)614         Builder setFadeManagerConfigurationEnabled(boolean enabled) {
615             mIsFadeManagerConfigurationEnabled = enabled;
616             return this;
617         }
618 
setDefaultCarAudioFadeConfiguration( CarAudioFadeConfiguration carAudioFadeConfiguration)619         Builder setDefaultCarAudioFadeConfiguration(
620                 CarAudioFadeConfiguration carAudioFadeConfiguration) {
621             mDefaultCarAudioFadeConfiguration = Objects.requireNonNull(carAudioFadeConfiguration,
622                     "Car audio fade configuration for default cannot be null");
623             return this;
624         }
625 
setCarAudioFadeConfigurationForAudioAttributes(AudioAttributes audioAttributes, CarAudioFadeConfiguration carAudioFadeConfiguration)626         Builder setCarAudioFadeConfigurationForAudioAttributes(AudioAttributes audioAttributes,
627                 CarAudioFadeConfiguration carAudioFadeConfiguration) {
628             Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null");
629             Objects.requireNonNull(carAudioFadeConfiguration,
630                     "Car audio fade configuration for audio attributes cannot be null");
631             mAudioAttributesToCarAudioFadeConfiguration.put(audioAttributes,
632                     carAudioFadeConfiguration);
633             return this;
634         }
635 
getZoneId()636         int getZoneId() {
637             return mZoneId;
638         }
639 
getZoneConfigId()640         int getZoneConfigId() {
641             return mZoneConfigId;
642         }
643 
build()644         CarAudioZoneConfig build() {
645             if (!mIsFadeManagerConfigurationEnabled) {
646                 mDefaultCarAudioFadeConfiguration = null;
647                 mAudioAttributesToCarAudioFadeConfiguration.clear();
648             }
649             return new CarAudioZoneConfig(mName, mZoneId, mZoneConfigId, mIsDefault, mVolumeGroups,
650                     mDeviceAddressToGroupId, mGroupIdToNames, mIsFadeManagerConfigurationEnabled,
651                     mDefaultCarAudioFadeConfiguration, mAudioAttributesToCarAudioFadeConfiguration);
652         }
653 
addGroupAddressesToMap(List<String> addresses, int groupId)654         private void addGroupAddressesToMap(List<String> addresses, int groupId) {
655             for (int index = 0; index < addresses.size(); index++) {
656                 mDeviceAddressToGroupId.put(addresses.get(index), groupId);
657             }
658         }
659     }
660 }
661