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 android.car.feature.Flags.carAudioDynamicDevices;
19 import static android.car.feature.Flags.carAudioMinMaxActivationVolume;
20 import static android.car.feature.Flags.carAudioMuteAmbiguity;
21 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_ATTENUATION_CHANGED;
22 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED;
23 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_BLOCKED_CHANGED;
24 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
25 import static android.media.AudioDeviceInfo.TYPE_AUX_LINE;
26 import static android.media.AudioDeviceInfo.TYPE_BLE_BROADCAST;
27 import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
28 import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
29 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
30 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
31 import static android.media.AudioDeviceInfo.TYPE_BUS;
32 import static android.media.AudioDeviceInfo.TYPE_HDMI;
33 import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY;
34 import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
35 import static android.media.AudioDeviceInfo.TYPE_USB_HEADSET;
36 import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
37 import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET;
38 
39 import static com.android.car.audio.CarActivationVolumeConfig.ActivationVolumeInvocationType;
40 import static com.android.car.audio.hal.HalAudioGainCallback.reasonToString;
41 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
42 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
43 
44 import android.annotation.NonNull;
45 import android.annotation.Nullable;
46 import android.annotation.UserIdInt;
47 import android.car.builtin.util.Slogf;
48 import android.car.media.CarVolumeGroupInfo;
49 import android.media.AudioAttributes;
50 import android.media.AudioDeviceAttributes;
51 import android.media.AudioDeviceInfo;
52 import android.media.AudioManager;
53 import android.os.UserHandle;
54 import android.util.ArrayMap;
55 import android.util.ArraySet;
56 import android.util.SparseArray;
57 import android.util.proto.ProtoOutputStream;
58 
59 import com.android.car.CarLog;
60 import com.android.car.audio.CarAudioContext.AudioContext;
61 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneConfigProto;
62 import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto;
63 import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto.ContextToAddress;
64 import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto.GainInfo;
65 import com.android.car.audio.hal.HalAudioDeviceInfo;
66 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
67 import com.android.car.internal.util.DebugUtils;
68 import com.android.car.internal.util.IndentingPrintWriter;
69 import com.android.internal.annotations.GuardedBy;
70 import com.android.internal.util.Preconditions;
71 
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.List;
75 import java.util.Objects;
76 import java.util.Set;
77 
78 /**
79  * A class encapsulates a volume group in car.
80  *
81  * Interface holding volume interface APIs and also common code for:
82  *
83  * -volume groups using {@link AudioManager#setAudioPortGain} to control the volume
84  * while the audioserver resource config_useFixedVolume is set.
85  *
86  * -volume groups relying on audioserver to control the volume and access using
87  * {@link AudioManager#setVolumeIndexForAttributes(AudioAttributes, int, int)} and all other
88  * related volume APIs.
89  * Gain may either be controlled on hardware amplifier using Audio HAL setaudioPortConfig if the
90  * correlated audio device port defines a gain controller with attribute name="useForVolume" set
91  * or in software using the port id in Audio flinger.
92  * Gains are set only when activity is detected on the given audio device port (Mixer thread, or
93  * {@link android.media.HwAudioSource} realized through a software bridge or hardware bridge.
94  *
95  */
96 /* package */ abstract class CarVolumeGroup {
97     public static final int UNINITIALIZED = -1;
98 
99     private final boolean mUseCarVolumeGroupMute;
100     private final boolean mHasCriticalAudioContexts;
101     private final CarAudioSettings mSettingsManager;
102     protected final int mId;
103     private final String mName;
104     protected final int mZoneId;
105     protected final int mConfigId;
106     protected final SparseArray<CarAudioDeviceInfo> mContextToDevices;
107 
108     protected final Object mLock = new Object();
109     private final CarAudioContext mCarAudioContext;
110 
111     private final CarActivationVolumeConfig mCarActivationVolumeConfig;
112 
113     @GuardedBy("mLock")
114     protected final SparseArray<String> mContextToAddress;
115     @GuardedBy("mLock")
116     protected final ArrayMap<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
117     @GuardedBy("mLock")
118     protected int mStoredGainIndex;
119 
120     @GuardedBy("mLock")
121     protected int mCurrentGainIndex = UNINITIALIZED;
122 
123     /**
124      * Mute state for requests coming from clients. See {@link #mIsHalMuted} for state of requests
125      * coming from HAL.
126      */
127     @GuardedBy("mLock")
128     protected boolean mIsMuted;
129     @GuardedBy("mLock")
130     protected @UserIdInt int mUserId = UserHandle.CURRENT.getIdentifier();
131 
132     /**
133      * Attenuated gain is set to {@link #UNINITIALIZED} till attenuation explicitly reported by
134      * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or
135      * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared,
136      * it returns back to {@link #UNINITIALIZED}.
137      */
138     @GuardedBy("mLock")
139     protected int mAttenuatedGainIndex = UNINITIALIZED;
140 
141     /**
142      * Limitation gain is set to max gain value till limitation explicitly reported by {@link
143      * com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or more
144      * {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared, it
145      * returns back to max.
146      */
147     @GuardedBy("mLock")
148     protected int mLimitedGainIndex;
149 
150     /**
151      * Blocked gain is set to {@link #UNINITIALIZED} till blocking case explicitly reported by
152      * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or
153      * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared,
154      * it returns back to {@link #UNINITIALIZED}.
155      */
156     @GuardedBy("mLock")
157     protected int mBlockedGainIndex = UNINITIALIZED;
158 
159     /**
160      * The default state of HAL mute is {@code false} until HAL explicitly reports through
161      * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or
162      * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason
163      * is cleared, it is reset. See {@link #mIsMuted} for state of requests coming from clients.
164      */
165     @GuardedBy("mLock")
166     private boolean mIsHalMuted = false;
167 
168     /**
169      * Reasons list currently reported for this port by {@link
170      * com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged}.
171      */
172     protected List<Integer> mReasons = new ArrayList<>();
173 
CarVolumeGroup(CarAudioContext carAudioContext, CarAudioSettings settingsManager, SparseArray<CarAudioDeviceInfo> contextToDevices, int zoneId, int configId, int volumeGroupId, String name, boolean useCarVolumeGroupMute, CarActivationVolumeConfig carActivationVolumeConfig)174     protected CarVolumeGroup(CarAudioContext carAudioContext, CarAudioSettings settingsManager,
175             SparseArray<CarAudioDeviceInfo> contextToDevices, int zoneId, int configId,
176             int volumeGroupId, String name, boolean useCarVolumeGroupMute,
177             CarActivationVolumeConfig carActivationVolumeConfig) {
178         mSettingsManager = settingsManager;
179         mCarAudioContext = carAudioContext;
180         mContextToDevices = contextToDevices;
181         mZoneId = zoneId;
182         mConfigId = configId;
183         mId = volumeGroupId;
184         mName = Objects.requireNonNull(name, "Volume group name cannot be null");
185         mUseCarVolumeGroupMute = useCarVolumeGroupMute;
186         mContextToAddress = new SparseArray<>(contextToDevices.size());
187         mAddressToCarAudioDeviceInfo = new ArrayMap<>(contextToDevices.size());
188         List<AudioAttributes> volumeAttributes = new ArrayList<>();
189         for (int index = 0; index <  contextToDevices.size(); index++) {
190             int context = contextToDevices.keyAt(index);
191             CarAudioDeviceInfo info = contextToDevices.valueAt(index);
192             List<AudioAttributes> audioAttributes =
193                     Arrays.asList(mCarAudioContext.getAudioAttributesForContext(context));
194             volumeAttributes.addAll(audioAttributes);
195             mContextToAddress.put(context, info.getAddress());
196             mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
197         }
198 
199         mHasCriticalAudioContexts = containsCriticalAttributes(volumeAttributes);
200         mCarActivationVolumeConfig = Objects.requireNonNull(carActivationVolumeConfig,
201                 "Activation volume config can not be null");
202     }
203 
init()204     void init() {
205         synchronized (mLock) {
206             mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(
207                     mUserId, mZoneId, mConfigId, mId);
208             updateCurrentGainIndexLocked();
209         }
210     }
211 
212     @GuardedBy("mLock")
setBlockedLocked(int blockedIndex)213     protected void setBlockedLocked(int blockedIndex) {
214         mBlockedGainIndex = blockedIndex;
215     }
216 
217     @GuardedBy("mLock")
resetBlockedLocked()218     protected void resetBlockedLocked() {
219         setBlockedLocked(UNINITIALIZED);
220     }
221 
222     @GuardedBy("mLock")
isBlockedLocked()223     protected boolean isBlockedLocked() {
224         return mBlockedGainIndex != UNINITIALIZED;
225     }
226 
227     @GuardedBy("mLock")
setLimitLocked(int limitIndex)228     protected void setLimitLocked(int limitIndex) {
229         int minActivationGainIndex = getMinActivationGainIndex();
230         if (limitIndex < minActivationGainIndex) {
231             Slogf.w(CarLog.TAG_AUDIO, "Limit cannot be set lower than min activation volume index",
232                     minActivationGainIndex);
233         }
234         mLimitedGainIndex = limitIndex;
235     }
236 
237     @GuardedBy("mLock")
resetLimitLocked()238     protected void resetLimitLocked() {
239         setLimitLocked(getMaxGainIndex());
240     }
241 
242     @GuardedBy("mLock")
isLimitedLocked()243     protected boolean isLimitedLocked() {
244         return mLimitedGainIndex != getMaxGainIndex();
245     }
246 
247     @GuardedBy("mLock")
isOverLimitLocked()248     protected boolean isOverLimitLocked() {
249         return isOverLimitLocked(mCurrentGainIndex);
250     }
251 
252     @GuardedBy("mLock")
isOverLimitLocked(int index)253     protected boolean isOverLimitLocked(int index) {
254         return isLimitedLocked() && (index > mLimitedGainIndex);
255     }
256 
257     @GuardedBy("mLock")
setAttenuatedGainLocked(int attenuatedGainIndex)258     protected void setAttenuatedGainLocked(int attenuatedGainIndex) {
259         mAttenuatedGainIndex = attenuatedGainIndex;
260     }
261 
262     @GuardedBy("mLock")
resetAttenuationLocked()263     protected void resetAttenuationLocked() {
264         setAttenuatedGainLocked(UNINITIALIZED);
265     }
266 
267     @GuardedBy("mLock")
isAttenuatedLocked()268     protected boolean isAttenuatedLocked() {
269         return mAttenuatedGainIndex != UNINITIALIZED;
270     }
271 
272     @GuardedBy("mLock")
setHalMuteLocked(boolean mute)273     private void setHalMuteLocked(boolean mute) {
274         mIsHalMuted = mute;
275     }
276 
277     @GuardedBy("mLock")
isHalMutedLocked()278     protected boolean isHalMutedLocked() {
279         return mIsHalMuted;
280     }
281 
isHalMuted()282     boolean isHalMuted() {
283         synchronized (mLock) {
284             return isHalMutedLocked();
285         }
286     }
287 
setBlocked(int blockedIndex)288     void setBlocked(int blockedIndex) {
289         synchronized (mLock) {
290             setBlockedLocked(blockedIndex);
291         }
292     }
293 
resetBlocked()294     void resetBlocked() {
295         synchronized (mLock) {
296             resetBlockedLocked();
297         }
298     }
299 
isBlocked()300     boolean isBlocked() {
301         synchronized (mLock) {
302             return isBlockedLocked();
303         }
304     }
305 
setLimit(int limitIndex)306     void setLimit(int limitIndex) {
307         synchronized (mLock) {
308             setLimitLocked(limitIndex);
309         }
310     }
311 
resetLimit()312     void resetLimit() {
313         synchronized (mLock) {
314             resetLimitLocked();
315         }
316     }
317 
isLimited()318     boolean isLimited() {
319         synchronized (mLock) {
320             return isLimitedLocked();
321         }
322     }
323 
isOverLimit()324     boolean isOverLimit() {
325         synchronized (mLock) {
326             return isOverLimitLocked();
327         }
328     }
329 
setAttenuatedGain(int attenuatedGainIndex)330     void setAttenuatedGain(int attenuatedGainIndex) {
331         synchronized (mLock) {
332             setAttenuatedGainLocked(attenuatedGainIndex);
333         }
334     }
335 
resetAttenuation()336     void resetAttenuation() {
337         synchronized (mLock) {
338             resetAttenuationLocked();
339         }
340     }
341 
isAttenuated()342     boolean isAttenuated() {
343         synchronized (mLock) {
344             return isAttenuatedLocked();
345         }
346     }
347 
348     @Nullable
getCarAudioDeviceInfoForAddress(String address)349     CarAudioDeviceInfo getCarAudioDeviceInfoForAddress(String address) {
350         synchronized (mLock) {
351             return mAddressToCarAudioDeviceInfo.get(address);
352         }
353     }
354 
getContexts()355     int[] getContexts() {
356         int[] carAudioContexts = new int[mContextToDevices.size()];
357         for (int i = 0; i < mContextToDevices.size(); i++) {
358             carAudioContexts[i] = mContextToDevices.keyAt(i);
359         }
360         return carAudioContexts;
361     }
362 
getAudioAttributesForContext(int context)363     protected AudioAttributes[] getAudioAttributesForContext(int context) {
364         return mCarAudioContext.getAudioAttributesForContext(context);
365     }
366 
367     /**
368      * Returns the id of the volume group.
369      * <p> Note that all clients are already developed in the way that when they get the number of
370      * volume group, they will then address a given volume group using its id as if the id was the
371      * index of the array of group (aka 0 to length - 1).
372      */
getId()373     int getId() {
374         return mId;
375     }
376 
getName()377     String getName() {
378         return mName;
379     }
380 
381     /**
382      * Returns the devices address for the given context
383      * or {@code null} if the context does not exist in the volume group
384      */
385     @Nullable
getAddressForContext(int audioContext)386     String getAddressForContext(int audioContext) {
387         synchronized (mLock) {
388             return mContextToAddress.get(audioContext);
389         }
390     }
391 
392     /**
393      * Returns the audio devices for the given context
394      * or {@code null} if the context does not exist in the volume group
395      */
396     @Nullable
getAudioDeviceForContext(int audioContext)397     AudioDeviceAttributes getAudioDeviceForContext(int audioContext) {
398         String address = getAddressForContext(audioContext);
399         if (address == null) {
400             return null;
401         }
402 
403         CarAudioDeviceInfo info;
404         synchronized (mLock) {
405             info = mAddressToCarAudioDeviceInfo.get(address);
406         }
407         if (info == null) {
408             return null;
409         }
410 
411         return info.getAudioDevice();
412     }
413 
414     @AudioContext
getContextsForAddress(@onNull String address)415     List<Integer> getContextsForAddress(@NonNull String address) {
416         List<Integer> carAudioContexts = new ArrayList<>();
417         synchronized (mLock) {
418             for (int i = 0; i < mContextToAddress.size(); i++) {
419                 String value = mContextToAddress.valueAt(i);
420                 if (address.equals(value)) {
421                     carAudioContexts.add(mContextToAddress.keyAt(i));
422                 }
423             }
424         }
425         return carAudioContexts;
426     }
427 
getAddresses()428     List<String> getAddresses() {
429         synchronized (mLock) {
430             return new ArrayList<>(mAddressToCarAudioDeviceInfo.keySet());
431         }
432     }
433 
getAllSupportedUsagesForAddress(@onNull String address)434     List<Integer> getAllSupportedUsagesForAddress(@NonNull String address) {
435         List<Integer> supportedUsagesForAddress = new ArrayList<>();
436         List<Integer> contextsForAddress = getContextsForAddress(address);
437         for (int contextIndex = 0; contextIndex < contextsForAddress.size(); contextIndex++) {
438             int contextId = contextsForAddress.get(contextIndex);
439             AudioAttributes[] attributes =
440                     mCarAudioContext.getAudioAttributesForContext(contextId);
441             for (int attrIndex = 0; attrIndex < attributes.length; attrIndex++) {
442                 int usage = attributes[attrIndex].getSystemUsage();
443                 if (!supportedUsagesForAddress.contains(usage)) {
444                     supportedUsagesForAddress.add(usage);
445                 }
446             }
447         }
448         return supportedUsagesForAddress;
449     }
450 
getMaxGainIndex()451     abstract int getMaxGainIndex();
452 
getMinGainIndex()453     abstract int getMinGainIndex();
454 
getMaxActivationGainIndex()455     int getMaxActivationGainIndex() {
456         int maxGainIndex = getMaxGainIndex();
457         int minGainIndex = getMinGainIndex();
458         return minGainIndex + (int) Math.round(
459                 mCarActivationVolumeConfig.getMaxActivationVolumePercentage() / 100.0
460                 * (maxGainIndex - minGainIndex));
461     }
462 
getMinActivationGainIndex()463     int getMinActivationGainIndex() {
464         int maxGainIndex = getMaxGainIndex();
465         int minGainIndex = getMinGainIndex();
466         return minGainIndex + (int) Math.round(
467                 mCarActivationVolumeConfig.getMinActivationVolumePercentage() / 100.0
468                 * (maxGainIndex - minGainIndex));
469     }
470 
getActivationVolumeInvocationType()471     int getActivationVolumeInvocationType() {
472         return mCarActivationVolumeConfig.getInvocationType();
473     }
474 
getCurrentGainIndex()475     int getCurrentGainIndex() {
476         synchronized (mLock) {
477             if (isMutedLocked()) {
478                 return getMinGainIndex();
479             }
480 
481             return getRestrictedGainForIndexLocked(getCurrentGainIndexLocked());
482         }
483     }
484 
485     @GuardedBy("mLock")
getCurrentGainIndexLocked()486     protected int getCurrentGainIndexLocked() {
487         return mCurrentGainIndex;
488     }
489 
490     @GuardedBy("mLock")
getRestrictedGainForIndexLocked(int index)491     protected int getRestrictedGainForIndexLocked(int index) {
492         if (isBlockedLocked()) {
493             return mBlockedGainIndex;
494         }
495         if (isOverLimitLocked()) {
496             return mLimitedGainIndex;
497         }
498         if (isAttenuatedLocked()) {
499             // Need to figure out if attenuation shall be hidden to end user
500             // as while ducked from IAudioControl
501             // TODO(b/) clarify in case of volume adjustment if the reference index is the
502             // ducked index or the current index. Taking current may lead to gap of index > 1.
503             return mAttenuatedGainIndex;
504         }
505         return index;
506     }
507 
508     /**
509      * Sets the gain on this group, gain will be set on all devices within volume group.
510      */
setCurrentGainIndex(int gainIndex)511     void setCurrentGainIndex(int gainIndex) {
512         synchronized (mLock) {
513             int currentgainIndex = gainIndex;
514             Preconditions.checkArgument(isValidGainIndexLocked(gainIndex),
515                     "Gain out of range (%d:%d) index %d", getMinGainIndex(), getMaxGainIndex(),
516                     gainIndex);
517             if (isBlockedLocked()) {
518                 // prevent any volume change while {@link IAudioGainCallback} reported block event.
519                 return;
520             }
521             if (isOverLimitLocked(currentgainIndex)) {
522                 currentgainIndex = mLimitedGainIndex;
523             }
524             if (isAttenuatedLocked()) {
525                 resetAttenuationLocked();
526             }
527             // In case of attenuation/Limitation, requested index is now the new reference for
528             // cached current index.
529             mCurrentGainIndex = currentgainIndex;
530 
531             if (mIsMuted) {
532                 setMuteLocked(false);
533             }
534             setCurrentGainIndexLocked(mCurrentGainIndex);
535         }
536     }
537 
538     @GuardedBy("mLock")
setCurrentGainIndexLocked(int gainIndex)539     protected void setCurrentGainIndexLocked(int gainIndex) {
540         storeGainIndexForUserLocked(gainIndex, mUserId);
541     }
542 
handleActivationVolume( @ctivationVolumeInvocationType int activationVolumeInvocationType)543     boolean handleActivationVolume(
544             @ActivationVolumeInvocationType int activationVolumeInvocationType) {
545         if (!carAudioMinMaxActivationVolume()
546                 || (getActivationVolumeInvocationType() & activationVolumeInvocationType) == 0) {
547             // Min/max activation volume is not invoked if the given invocation type is not allowed
548             // for the volume group.
549             return false;
550         }
551         boolean invokeVolumeGainIndexChanged = true;
552         synchronized (mLock) {
553             int minActivationGainIndex = getMinActivationGainIndex();
554             int maxActivationGainIndex = getMaxActivationGainIndex();
555             int curGainIndex = getCurrentGainIndexLocked();
556             int activationVolume;
557             if (curGainIndex > maxActivationGainIndex) {
558                 activationVolume = maxActivationGainIndex;
559             } else if (curGainIndex < minActivationGainIndex) {
560                 activationVolume = minActivationGainIndex;
561             } else {
562                 return false;
563             }
564             if (isMutedLocked() || isBlockedLocked()) {
565                 invokeVolumeGainIndexChanged = false;
566             } else {
567                 if (isOverLimitLocked(activationVolume)) {
568                     // Limit index is used as min activation gain index if limit is lower than min
569                     // activation gain index.
570                     invokeVolumeGainIndexChanged = !isOverLimitLocked(curGainIndex);
571                 }
572                 if (isAttenuatedLocked()) {
573                     // Attenuation state should be maintained and not reset for min/max activation.
574                     invokeVolumeGainIndexChanged = false;
575                 }
576             }
577             mCurrentGainIndex = activationVolume;
578             setCurrentGainIndexLocked(mCurrentGainIndex);
579         }
580         return invokeVolumeGainIndexChanged;
581     }
582 
hasCriticalAudioContexts()583     boolean hasCriticalAudioContexts() {
584         return mHasCriticalAudioContexts;
585     }
586 
587     @Override
588     @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
toString()589     public String toString() {
590         synchronized (mLock) {
591             return "CarVolumeGroup id: " + mId
592                     + " currentGainIndex: " + mCurrentGainIndex
593                     + " contexts: " + Arrays.toString(getContexts())
594                     + " addresses: " + String.join(", ", getAddresses());
595         }
596     }
597 
598     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpLocked(IndentingPrintWriter writer)599     protected abstract void dumpLocked(IndentingPrintWriter writer);
600 
601     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)602     void dump(IndentingPrintWriter writer) {
603         synchronized (mLock) {
604             writer.printf("CarVolumeGroup(%d)\n", mId);
605             writer.increaseIndent();
606             writer.printf("Name(%s)\n", mName);
607             writer.printf("Zone Id(%d)\n", mZoneId);
608             writer.printf("Configuration Id(%d)\n", mConfigId);
609             writer.printf("Is Muted(%b)\n", isMutedLocked());
610             writer.printf("UserId(%d)\n", mUserId);
611             writer.printf("Persist Volume Group Mute(%b)\n",
612                     mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId));
613             dumpLocked(writer);
614             writer.printf("Gain indexes (min / max / default / current): %d %d %d %d\n",
615                     getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(),
616                     mCurrentGainIndex);
617             writer.printf("Activation gain (min index / max index / invocation type): %d %d %d\n",
618                     getMinActivationGainIndex(), getMaxActivationGainIndex(),
619                     getActivationVolumeInvocationType());
620             for (int i = 0; i < mContextToAddress.size(); i++) {
621                 writer.printf("Context: %s -> Address: %s\n",
622                         mCarAudioContext.toString(mContextToAddress.keyAt(i)),
623                         mContextToAddress.valueAt(i));
624             }
625             for (int i = 0; i < mContextToDevices.size(); i++) {
626                 CarAudioDeviceInfo info = mContextToDevices.valueAt(i);
627                 info.dump(writer);
628             }
629             writer.printf("Reported reasons:\n");
630             writer.increaseIndent();
631             for (int index = 0; index < mReasons.size(); index++) {
632                 int reason = mReasons.get(index);
633                 writer.printf("%s\n", reasonToString(reason));
634             }
635             writer.decreaseIndent();
636             writer.printf("Gain infos:\n");
637             writer.increaseIndent();
638             writer.printf(
639                     "Blocked: %b%s\n",
640                     isBlockedLocked(),
641                     (isBlockedLocked() ? " (at: " + mBlockedGainIndex + ")" : ""));
642             writer.printf(
643                     "Limited: %b%s\n",
644                     isLimitedLocked(),
645                     (isLimitedLocked() ? " (at: " + mLimitedGainIndex + ")" : ""));
646             writer.printf(
647                     "Attenuated: %b%s\n",
648                     isAttenuatedLocked(),
649                     (isAttenuatedLocked() ? " (at: " + mAttenuatedGainIndex + ")" : ""));
650             writer.printf("Muted by HAL: %b\n", isHalMutedLocked());
651             writer.decreaseIndent();
652             // Empty line for comfortable reading
653             writer.println();
654             writer.decreaseIndent();
655         }
656     }
657 
658     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)659     void dumpProto(ProtoOutputStream proto) {
660         long volumeGroupToken = proto.start(CarAudioZoneConfigProto.VOLUME_GROUPS);
661         synchronized (mLock) {
662             proto.write(CarVolumeGroupProto.ID, mId);
663             proto.write(CarVolumeGroupProto.NAME, mName);
664             proto.write(CarVolumeGroupProto.ZONE_ID, mZoneId);
665             proto.write(CarVolumeGroupProto.CONFIG_ID, mConfigId);
666             proto.write(CarVolumeGroupProto.MUTED, isMutedLocked());
667             proto.write(CarVolumeGroupProto.USER_ID, mUserId);
668             proto.write(CarVolumeGroupProto.PERSIST_VOLUME_GROUP_MUTE_ENABLED,
669                     mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId));
670 
671             long volumeGainToken = proto.start(CarVolumeGroupProto.VOLUME_GAIN);
672             proto.write(CarAudioDumpProto.CarVolumeGain.MIN_GAIN_INDEX, getMinGainIndex());
673             proto.write(CarAudioDumpProto.CarVolumeGain.MAX_GAIN_INDEX, getMaxGainIndex());
674             proto.write(CarAudioDumpProto.CarVolumeGain.DEFAULT_GAIN_INDEX, getDefaultGainIndex());
675             proto.write(CarAudioDumpProto.CarVolumeGain.CURRENT_GAIN_INDEX, mCurrentGainIndex);
676             proto.write(CarAudioDumpProto.CarVolumeGain.MIN_ACTIVATION_GAIN_INDEX,
677                     getMinActivationGainIndex());
678             proto.write(CarAudioDumpProto.CarVolumeGain.MAX_ACTIVATION_GAIN_INDEX,
679                     getMaxActivationGainIndex());
680             proto.write(CarAudioDumpProto.CarVolumeGain.ACTIVATION_INVOCATION_TYPE,
681                     getActivationVolumeInvocationType());
682             proto.end(volumeGainToken);
683 
684             for (int i = 0; i < mContextToAddress.size(); i++) {
685                 long contextToAddressMappingToken = proto.start(CarVolumeGroupProto
686                         .CONTEXT_TO_ADDRESS_MAPPINGS);
687                 proto.write(ContextToAddress.CONTEXT,
688                         mCarAudioContext.toString(mContextToAddress.keyAt(i)));
689                 proto.write(ContextToAddress.ADDRESS, mContextToAddress.valueAt(i));
690                 proto.end(contextToAddressMappingToken);
691             }
692 
693             for (int i = 0; i < mContextToDevices.size(); i++) {
694                 CarAudioDeviceInfo info = mContextToDevices.valueAt(i);
695                 info.dumpProto(CarVolumeGroupProto.CAR_AUDIO_DEVICE_INFOS, proto);
696             }
697 
698             for (int index = 0; index < mReasons.size(); index++) {
699                 int reason = mReasons.get(index);
700                 proto.write(CarVolumeGroupProto.REPORTED_REASONS, reasonToString(reason));
701             }
702 
703             long gainInfoToken = proto.start(CarVolumeGroupProto.GAIN_INFOS);
704             proto.write(GainInfo.BLOCKED, isBlockedLocked());
705             if (isBlockedLocked()) {
706                 proto.write(GainInfo.BLOCKED_GAIN_INDEX, mBlockedGainIndex);
707             }
708             proto.write(GainInfo.LIMITED, isLimitedLocked());
709             if (isLimitedLocked()) {
710                 proto.write(GainInfo.LIMITED_GAIN_INDEX, mLimitedGainIndex);
711             }
712             proto.write(GainInfo.ATTENUATED, isAttenuatedLocked());
713             if (isAttenuatedLocked()) {
714                 proto.write(GainInfo.ATTENUATED_GAIN_INDEX, mAttenuatedGainIndex);
715             }
716             proto.write(GainInfo.HAL_MUTED, isHalMutedLocked());
717             proto.write(GainInfo.IS_ACTIVE, isActive());
718             proto.end(gainInfoToken);
719 
720         }
721         proto.end(volumeGroupToken);
722     }
723 
loadVolumesSettingsForUser(@serIdInt int userId)724     void loadVolumesSettingsForUser(@UserIdInt int userId) {
725         synchronized (mLock) {
726             //Update the volume for the new user
727             updateUserIdLocked(userId);
728             //Update the current gain index
729             updateCurrentGainIndexLocked();
730             setCurrentGainIndexLocked(getCurrentGainIndexLocked());
731             //Reset devices with current gain index
732             updateGroupMuteLocked();
733         }
734     }
735 
736     /**
737      * Set the mute state of the Volume Group
738      *
739      * @param mute state requested
740      * @return true if mute state has changed, false otherwiser (already set or change not allowed)
741      */
setMute(boolean mute)742     boolean setMute(boolean mute) {
743         synchronized (mLock) {
744             // if hal muted the audio devices, then do not allow other incoming requests
745             // to perform unmute.
746             if (!mute && isHalMutedLocked()) {
747                 Slogf.e(CarLog.TAG_AUDIO, "Un-mute request cannot be processed due to active "
748                         + "hal mute restriction!");
749                 return false;
750             }
751             applyMuteLocked(mute);
752             return setMuteLocked(mute);
753         }
754     }
755 
756     @GuardedBy("mLock")
setMuteLocked(boolean mute)757     protected boolean setMuteLocked(boolean mute) {
758         boolean hasChanged = mIsMuted != mute;
759         mIsMuted = mute;
760         if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
761             mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mConfigId, mId, mute);
762         }
763         return hasChanged;
764     }
765 
766     @GuardedBy("mLock")
applyMuteLocked(boolean mute)767     protected void applyMuteLocked(boolean mute) {
768     }
769 
isMuted()770     boolean isMuted() {
771         synchronized (mLock) {
772             return isMutedLocked();
773         }
774     }
775 
776     @GuardedBy("mLock")
isMutedLocked()777     protected boolean isMutedLocked() {
778         // if either of the mute states is set, it results in group being muted.
779         return isUserMutedLocked() || isHalMutedLocked();
780     }
781 
782     @GuardedBy("mLock")
isUserMutedLocked()783     protected boolean isUserMutedLocked() {
784         return mIsMuted;
785     }
786 
787     @GuardedBy("mLock")
isFullyMutedLocked()788     protected boolean isFullyMutedLocked() {
789         return isUserMutedLocked() || isHalMutedLocked() || isBlockedLocked();
790     }
791 
containsCriticalAttributes(List<AudioAttributes> volumeAttributes)792     private static boolean containsCriticalAttributes(List<AudioAttributes> volumeAttributes) {
793         for (int index = 0; index < volumeAttributes.size(); index++) {
794             if (CarAudioContext.isCriticalAudioAudioAttribute(volumeAttributes.get(index))) {
795                 return true;
796             }
797         }
798         return false;
799     }
800 
801     @GuardedBy("mLock")
updateUserIdLocked(@serIdInt int userId)802     private void updateUserIdLocked(@UserIdInt int userId) {
803         mUserId = userId;
804         mStoredGainIndex = getCurrentGainIndexForUserLocked();
805     }
806 
807     @GuardedBy("mLock")
getCurrentGainIndexForUserLocked()808     private int getCurrentGainIndexForUserLocked() {
809         int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId,
810                 mConfigId, mId);
811         Slogf.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId
812                 + " gainIndexForUser " + gainIndexForUser);
813         return gainIndexForUser;
814     }
815 
816     /**
817      * Update the current gain index based on the stored gain index
818      */
819     @GuardedBy("mLock")
updateCurrentGainIndexLocked()820     private void updateCurrentGainIndexLocked() {
821         if (isValidGainIndexLocked(mStoredGainIndex)) {
822             mCurrentGainIndex = mStoredGainIndex;
823         } else {
824             mCurrentGainIndex = getDefaultGainIndex();
825         }
826     }
827 
isValidGainIndex(int gainIndex)828     protected boolean isValidGainIndex(int gainIndex) {
829         synchronized (mLock) {
830             return isValidGainIndexLocked(gainIndex);
831         }
832     }
isValidGainIndexLocked(int gainIndex)833     protected abstract boolean isValidGainIndexLocked(int gainIndex);
834 
getDefaultGainIndex()835     protected abstract int getDefaultGainIndex();
836 
837     @GuardedBy("mLock")
storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId)838     private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) {
839         mSettingsManager.storeVolumeGainIndexForUser(userId,
840                 mZoneId, mConfigId, mId, gainIndex);
841     }
842 
843     @GuardedBy("mLock")
updateGroupMuteLocked()844     private void updateGroupMuteLocked() {
845         if (!mUseCarVolumeGroupMute) {
846             return;
847         }
848         if (!mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
849             mIsMuted = false;
850             return;
851         }
852         mIsMuted = mSettingsManager.getVolumeGroupMuteForUser(mUserId, mZoneId, mConfigId, mId);
853         applyMuteLocked(isFullyMutedLocked());
854     }
855 
856     /**
857      * Updates volume group states (index, mute, blocked etc) on callback from audio control hal.
858      *
859      * <p>If gain config info carries duplicate info, do not generate events (i.e. eventType = 0)
860      * @param halReasons reasons for change to gain config info
861      * @param gain updated gain config info
862      * @return one or more of {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum} or 0 for
863      * duplicate gain config info
864      */
onAudioGainChanged(List<Integer> halReasons, CarAudioGainConfigInfo gain)865     int onAudioGainChanged(List<Integer> halReasons, CarAudioGainConfigInfo gain) {
866         int eventType = 0;
867         int halIndex = gain.getVolumeIndex();
868         if (getCarAudioDeviceInfoForAddress(gain.getDeviceAddress()) == null
869                 || !isValidGainIndex(halIndex)) {
870             Slogf.e(CarLog.TAG_AUDIO,
871                     "onAudioGainChanged invalid CarAudioGainConfigInfo: " + gain
872                     + " for group id: " + mId);
873             return eventType;
874         }
875         synchronized (mLock) {
876             int previousRestrictedIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex);
877             mReasons = new ArrayList<>(halReasons);
878 
879             boolean shouldBlock = CarAudioGainMonitor.shouldBlockVolumeRequest(halReasons);
880             if ((shouldBlock != isBlockedLocked())
881                     || (shouldBlock && (halIndex != mBlockedGainIndex))) {
882                 setBlockedLocked(shouldBlock ? halIndex : UNINITIALIZED);
883                 eventType |= EVENT_TYPE_VOLUME_BLOCKED_CHANGED;
884             }
885 
886             boolean shouldLimit = CarAudioGainMonitor.shouldLimitVolume(halReasons);
887             if ((shouldLimit != isLimitedLocked())
888                     || (shouldLimit && (halIndex != mLimitedGainIndex))) {
889                 setLimitLocked(shouldLimit ? halIndex : getMaxGainIndex());
890                 eventType |= EVENT_TYPE_ATTENUATION_CHANGED;
891             }
892 
893             boolean shouldDuck = CarAudioGainMonitor.shouldDuckGain(halReasons);
894             if ((shouldDuck != isAttenuatedLocked())
895                     || (shouldDuck && (halIndex != mAttenuatedGainIndex))) {
896                 setAttenuatedGainLocked(shouldDuck ? halIndex : UNINITIALIZED);
897                 eventType |= EVENT_TYPE_ATTENUATION_CHANGED;
898             }
899 
900             // Accept mute callbacks from hal only if group mute is enabled.
901             // If disabled, such callbacks will be considered as blocking restriction only.
902             boolean shouldMute = CarAudioGainMonitor.shouldMuteVolumeGroup(halReasons);
903             if (mUseCarVolumeGroupMute && (shouldMute != isHalMutedLocked())) {
904                 setHalMuteLocked(shouldMute);
905                 eventType |= EVENT_TYPE_MUTE_CHANGED;
906             }
907 
908             if (CarAudioGainMonitor.shouldUpdateVolumeIndex(halReasons)
909                     && (halIndex != getRestrictedGainForIndexLocked(mCurrentGainIndex))) {
910                 mCurrentGainIndex = halIndex;
911                 eventType |= EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
912             }
913 
914             // Blocked/Attenuated index shall have been already apply by Audio HAL on HW.
915             // However, keep in sync & broadcast to all ports this volume group deals with.
916             //
917             // Do not update current gain cache, keep it for restoring rather using reported index
918             // when the event is cleared.
919             int newRestrictedIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex);
920             setCurrentGainIndexLocked(newRestrictedIndex);
921             // Hal or user mute state can change (only user mute enabled while hal muted allowed).
922             // Force a sync of mute application.
923             applyMuteLocked(isFullyMutedLocked());
924 
925             if (newRestrictedIndex != previousRestrictedIndex) {
926                 eventType |= EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
927             }
928         }
929         return eventType;
930     }
931 
getCarVolumeGroupInfo()932     CarVolumeGroupInfo getCarVolumeGroupInfo() {
933         int gainIndex;
934         boolean isMuted;
935         boolean isHalMuted;
936         boolean isBlocked;
937         boolean isAttenuated;
938         synchronized (mLock) {
939             gainIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex);
940             isMuted = isMutedLocked();
941             isHalMuted = isHalMutedLocked();
942             isBlocked = isBlockedLocked();
943             isAttenuated = isAttenuatedLocked() || isLimitedLocked();
944         }
945 
946         String name = mName.isEmpty() ? "group id " + mId : mName;
947 
948         CarVolumeGroupInfo.Builder builder = new CarVolumeGroupInfo.Builder(name, mZoneId, mId)
949                 .setVolumeGainIndex(gainIndex).setMaxVolumeGainIndex(getMaxGainIndex())
950                 .setMinVolumeGainIndex(getMinGainIndex()).setMuted(isMuted).setBlocked(isBlocked)
951                 .setAttenuated(isAttenuated).setAudioAttributes(getAudioAttributes());
952 
953         if (carAudioDynamicDevices()) {
954             builder.setAudioDeviceAttributes(getAudioDeviceAttributes());
955         }
956 
957         if (carAudioMinMaxActivationVolume()) {
958             builder.setMaxActivationVolumeGainIndex(getMaxActivationGainIndex())
959                     .setMinActivationVolumeGainIndex(getMinActivationGainIndex());
960         }
961 
962         if (carAudioMuteAmbiguity()) {
963             builder.setMutedBySystem(isHalMuted);
964         }
965 
966         return builder.build();
967     }
968 
getAudioDeviceAttributes()969     private List<AudioDeviceAttributes> getAudioDeviceAttributes() {
970         ArraySet<AudioDeviceAttributes> set = new ArraySet<>();
971         int[] contexts = getContexts();
972         for (int index = 0; index < contexts.length; index++) {
973             AudioDeviceAttributes device = getAudioDeviceForContext(contexts[index]);
974             if (device == null) {
975                 Slogf.w(CarLog.TAG_AUDIO,
976                         "getAudioDeviceAttributes: Could not find audio device for context "
977                                 + mCarAudioContext.toString(contexts[index]));
978                 continue;
979             }
980             set.add(device);
981         }
982         return new ArrayList<>(set);
983     }
984 
hasAudioAttributes(AudioAttributes audioAttributes)985     boolean hasAudioAttributes(AudioAttributes audioAttributes) {
986         synchronized (mLock) {
987             return mContextToAddress.contains(mCarAudioContext.getContextForAttributes(
988                     audioAttributes));
989         }
990     }
991 
getAudioAttributes()992     List<AudioAttributes> getAudioAttributes() {
993         List<AudioAttributes> audioAttributes = new ArrayList<>();
994         synchronized (mLock) {
995             for (int index = 0; index < mContextToAddress.size(); index++) {
996                 int context = mContextToAddress.keyAt(index);
997                 AudioAttributes[] contextAttributes =
998                         mCarAudioContext.getAudioAttributesForContext(context);
999                 for (int attrIndex = 0; attrIndex < contextAttributes.length; attrIndex++) {
1000                     audioAttributes.add(contextAttributes[attrIndex]);
1001                 }
1002             }
1003         }
1004         return audioAttributes;
1005     }
1006 
1007     /**
1008      * @return one or more {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum}
1009      */
onAudioVolumeGroupChanged(int flags)1010     public int onAudioVolumeGroupChanged(int flags) {
1011         return 0;
1012     }
1013 
1014     /**
1015      * Updates car audio device info with the hal audio device info
1016      */
updateAudioDeviceInfo(HalAudioDeviceInfo halDeviceInfo)1017     void updateAudioDeviceInfo(HalAudioDeviceInfo halDeviceInfo) {
1018         synchronized (mLock) {
1019             CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(halDeviceInfo.getAddress());
1020             if (info == null) {
1021                 Slogf.w(CarLog.TAG_AUDIO, "No matching car audio device info found for address: %s",
1022                         halDeviceInfo.getAddress());
1023                 return;
1024             }
1025             info.updateAudioDeviceInfo(halDeviceInfo);
1026         }
1027     }
1028 
updateDevices(boolean useCoreAudioRouting)1029     void updateDevices(boolean useCoreAudioRouting) {
1030     }
1031 
1032     /**
1033      * Calculates the new gain stages from list of assigned audio device infos
1034      *
1035      * <p>Used to update audio device gain stages dynamically.
1036      *
1037      * @return  one or more of {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum}, or 0 if
1038      * dynamic updates are not supported
1039      */
calculateNewGainStageFromDeviceInfos()1040     int calculateNewGainStageFromDeviceInfos() {
1041         return 0;
1042     }
1043 
isActive()1044     boolean isActive() {
1045         synchronized (mLock) {
1046             for (int c = 0; c < mAddressToCarAudioDeviceInfo.size(); c++) {
1047                 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.valueAt(c);
1048                 if (info.isActive()) {
1049                     continue;
1050                 }
1051                 return false;
1052             }
1053         }
1054         return true;
1055     }
1056 
audioDevicesAdded(List<AudioDeviceInfo> devices)1057     public boolean audioDevicesAdded(List<AudioDeviceInfo> devices) {
1058         Objects.requireNonNull(devices, "Audio devices can not be null");
1059         if (isActive()) {
1060             return false;
1061         }
1062 
1063         boolean updated = false;
1064         for (int c = 0; c < mContextToDevices.size(); c++) {
1065             if (!mContextToDevices.valueAt(c).audioDevicesAdded(devices)) {
1066                 continue;
1067             }
1068             updated = true;
1069         }
1070         if (!updated) {
1071             return false;
1072         }
1073         synchronized (mLock) {
1074             updateAudioDevicesMappingLocked();
1075         }
1076         return true;
1077     }
1078 
audioDevicesRemoved(List<AudioDeviceInfo> devices)1079     public boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) {
1080         Objects.requireNonNull(devices, "Audio devices can not be null");
1081         boolean updated = false;
1082         for (int c = 0; c < mContextToDevices.size(); c++) {
1083             if (!mContextToDevices.valueAt(c).audioDevicesRemoved(devices)) {
1084                 continue;
1085             }
1086             updated = true;
1087         }
1088         if (!updated) {
1089             return false;
1090         }
1091         synchronized (mLock) {
1092             updateAudioDevicesMappingLocked();
1093         }
1094         return true;
1095     }
1096 
1097     @GuardedBy("mLock")
updateAudioDevicesMappingLocked()1098     private void updateAudioDevicesMappingLocked() {
1099         mAddressToCarAudioDeviceInfo.clear();
1100         mContextToAddress.clear();
1101         for (int c = 0; c < mContextToDevices.size(); c++) {
1102             CarAudioDeviceInfo info = mContextToDevices.valueAt(c);
1103             int audioContext = mContextToDevices.keyAt(c);
1104             mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
1105             mContextToAddress.put(audioContext, info.getAddress());
1106         }
1107     }
1108 
1109     /**
1110      * Determines if device types assign to volume groups are valid based on the following rules:
1111      * <ul>
1112      * <li>Dynamic device types (non BUS) for this group should not appear in the
1113      * {@code dynamicDeviceTypesInConfig} passed in parameter</li>
1114      * <li>Dynamic device types should appear alone in volume group</li>
1115      * </ul>
1116      *
1117      * @param dynamicDeviceTypesInConfig Devices already seen in other volume groups for the same
1118      * configuration, groups checks if the device types for the volume group already exists here
1119      * and return {@code false} if so. Also adds any non-existing device types for the group.
1120      * @return {@code true} if the rules defined above are valid for the group, {@code false}
1121      * otherwise
1122      */
validateDeviceTypes(Set<Integer> dynamicDeviceTypesInConfig)1123     boolean validateDeviceTypes(Set<Integer> dynamicDeviceTypesInConfig) {
1124         List<AudioDeviceAttributes> devices = getAudioDeviceAttributes();
1125         boolean hasNonBusDevice = false;
1126         for (int c = 0; c < devices.size(); c++) {
1127             int deviceType = devices.get(c).getType();
1128             // BUS devices are handled by address name check
1129             if (deviceType == TYPE_BUS) {
1130                 continue;
1131             }
1132             hasNonBusDevice = true;
1133             int convertedType = convertDeviceType(deviceType);
1134             if (dynamicDeviceTypesInConfig.add(convertedType)) {
1135                 continue;
1136             }
1137             Slogf.e(CarLog.TAG_AUDIO, "Car volume groups defined in"
1138                     + " car_audio_configuration.xml shared the dynamic device type "
1139                     + DebugUtils.constantToString(AudioDeviceInfo.class, /* prefix= */ "TYPE_",
1140                     deviceType) + " in multiple volume groups in the same configuration");
1141             return false;
1142         }
1143         if (!hasNonBusDevice || devices.size() == 1) {
1144             return true;
1145         }
1146         Slogf.e(CarLog.TAG_AUDIO, "Car volume group " + getName()
1147                 + " defined in car_audio_configuration.xml"
1148                 + " has multiple devices for a dynamic device group."
1149                 + " Groups with dynamic devices can only have a single device.");
1150         return false;
1151     }
1152 
1153     // Given the current limitation in BT stack where there can only be one BT device available
1154     // of any type, we need to consider all BT types as the same, we are picking TYPE_BLUETOOTH_A2DP
1155     // for verification purposes, could pick any of them.
convertDeviceType(int type)1156     private static int convertDeviceType(int type) {
1157         switch (type) {
1158             case TYPE_BLUETOOTH_A2DP: // fall through
1159             case TYPE_BLE_HEADSET: // fall through
1160             case TYPE_BLE_SPEAKER: // fall through
1161             case TYPE_BLE_BROADCAST:
1162                 return TYPE_BLUETOOTH_A2DP;
1163             case TYPE_BUILTIN_SPEAKER: // fall through
1164             case TYPE_WIRED_HEADSET: // fall through
1165             case TYPE_WIRED_HEADPHONES: // fall through
1166             case TYPE_HDMI: // fall through
1167             case TYPE_USB_ACCESSORY: // fall through
1168             case TYPE_USB_DEVICE: // fall through
1169             case TYPE_USB_HEADSET: // fall through
1170             case TYPE_AUX_LINE: // fall through
1171             case TYPE_BUS:
1172             default:
1173                 return type;
1174         }
1175     }
1176 }
1177