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