1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.audio;
18 
19 import android.annotation.NonNull;
20 import android.car.builtin.util.Slogf;
21 import android.car.media.CarVolumeGroupEvent;
22 import android.hardware.automotive.audiocontrol.Reasons;
23 import android.util.SparseArray;
24 import android.util.SparseIntArray;
25 
26 import com.android.car.CarLog;
27 import com.android.car.audio.hal.AudioControlWrapper;
28 import com.android.car.audio.hal.HalAudioGainCallback;
29 
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Objects;
33 
34 /**
35  * Provides audio gain callback registration helpers and implements AudioGain listener business
36  * logic.
37  */
38 /* package */ final class CarAudioGainMonitor {
39     private static final int INVALID_INFO = -1;
40     @NonNull private final AudioControlWrapper mAudioControlWrapper;
41     @NonNull private final SparseArray<CarAudioZone> mCarAudioZones;
42     private final CarVolumeInfoWrapper mCarVolumeInfoWrapper;
43 
CarAudioGainMonitor(AudioControlWrapper audioControlWrapper, CarVolumeInfoWrapper carVolumeInfoWrapper, SparseArray<CarAudioZone> carAudioZones)44     CarAudioGainMonitor(AudioControlWrapper audioControlWrapper,
45             CarVolumeInfoWrapper carVolumeInfoWrapper, SparseArray<CarAudioZone> carAudioZones) {
46         mAudioControlWrapper = Objects.requireNonNull(audioControlWrapper,
47                 "Audio Control Wrapper can not be null");
48         mCarVolumeInfoWrapper = Objects.requireNonNull(carVolumeInfoWrapper,
49                 "Car volume info wrapper can not be null");
50         mCarAudioZones = Objects.requireNonNull(carAudioZones, "Car Audio Zones can not be null");
51     }
52 
reset()53     public void reset() {
54         // TODO (b/224885748): handle specific logic on IAudioControl service died event
55     }
56 
57     /**
58      * Registers {@code HalAudioGainCallback} on {@code AudioControlWrapper} to receive HAL audio
59      * gain change notifications.
60      */
registerAudioGainListener(HalAudioGainCallback callback)61     public void registerAudioGainListener(HalAudioGainCallback callback) {
62         Objects.requireNonNull(callback, "Hal Audio Gain callback can not be null");
63         mAudioControlWrapper.registerAudioGainCallback(callback);
64     }
65 
66     /** Unregisters {@code HalAudioGainCallback} from {@code AudioControlWrapper}. */
unregisterAudioGainListener()67     public void unregisterAudioGainListener() {
68         mAudioControlWrapper.unregisterAudioGainCallback();
69     }
70 
71     /**
72      * Audio Gain event dispatcher. Implements the callback that triggered from {@link
73      * IAudioGainCallback#onAudioDeviceGainsChanged} with the list of reasons and the list of {@link
74      * CarAudioGainConfigInfo} involved. It is in charge of dispatching /delegating to the zone the
75      * {@link CarAudioGainConfigInfo} belongs the processing of the callback.
76      */
handleAudioDeviceGainsChanged(List<Integer> reasons, List<CarAudioGainConfigInfo> gains)77     void handleAudioDeviceGainsChanged(List<Integer> reasons, List<CarAudioGainConfigInfo> gains) {
78         List<CarVolumeGroupEvent> events = new ArrayList<>();
79         // Delegate to CarAudioZone / CarVolumeGroup
80         // Group gains by Audio Zones first
81         SparseArray<List<CarAudioGainConfigInfo>> gainsByZones = new SparseArray<>();
82         for (int index = 0; index < gains.size(); index++) {
83             CarAudioGainConfigInfo gain = gains.get(index);
84             int zone = gain.getZoneId();
85             if (!gainsByZones.contains(zone)) {
86                 gainsByZones.put(zone, new ArrayList<>(1));
87             }
88             gainsByZones.get(zone).add(gain);
89         }
90         for (int i = 0; i < gainsByZones.size(); i++) {
91             int zoneId = gainsByZones.keyAt(i);
92             if (!mCarAudioZones.contains(zoneId)) {
93                 Slogf.e(
94                         CarLog.TAG_AUDIO,
95                         "onAudioDeviceGainsChanged reported change on invalid "
96                                 + "zone: %d, reasons=%s, gains=%s",
97                         zoneId,
98                         reasons,
99                         gains);
100                 continue;
101             }
102             CarAudioZone carAudioZone = mCarAudioZones.get(zoneId);
103             events.addAll(carAudioZone.onAudioGainChanged(reasons, gainsByZones.valueAt(i)));
104         }
105 
106         // its possible we received redundant callbacks from hal. In such cases,
107         // do not call listeners with empty events.
108         if (events.isEmpty()) {
109             Slogf.w(CarLog.TAG_AUDIO, "Audio gain config callback resulted in no events!");
110             return;
111         }
112         mCarVolumeInfoWrapper.onVolumeGroupEvent(events);
113     }
114 
shouldBlockVolumeRequest(List<Integer> reasons)115     static boolean shouldBlockVolumeRequest(List<Integer> reasons) {
116         return reasons.contains(Reasons.FORCED_MASTER_MUTE) || reasons.contains(Reasons.TCU_MUTE)
117                 || reasons.contains(Reasons.REMOTE_MUTE);
118     }
119 
shouldLimitVolume(List<Integer> reasons)120     static boolean shouldLimitVolume(List<Integer> reasons) {
121         return reasons.contains(Reasons.THERMAL_LIMITATION)
122                 || reasons.contains(Reasons.SUSPEND_EXIT_VOL_LIMITATION);
123     }
124 
shouldDuckGain(List<Integer> reasons)125     static boolean shouldDuckGain(List<Integer> reasons) {
126         return reasons.contains(Reasons.ADAS_DUCKING) || reasons.contains(Reasons.NAV_DUCKING);
127     }
128 
shouldMuteVolumeGroup(List<Integer> reasons)129     static boolean shouldMuteVolumeGroup(List<Integer> reasons) {
130         return reasons.contains(Reasons.TCU_MUTE) || reasons.contains(Reasons.REMOTE_MUTE);
131     }
132 
shouldUpdateVolumeIndex(List<Integer> reasons)133     static boolean shouldUpdateVolumeIndex(List<Integer> reasons) {
134         return reasons.contains(Reasons.EXTERNAL_AMP_VOL_FEEDBACK);
135     }
136 
137     private static final SparseIntArray REASONS_TO_EXTRA_INFO = new SparseIntArray();
138 
139     // note: Reasons.FORCED_MASTER_MUTE, Reasons.OTHER are not supported by CarVolumeGroupEvent
140     //       extra-infos. Builder will automatically append EXTRA_INFO_NONE for these cases.
141     static {
REASONS_TO_EXTRA_INFO.put(Reasons.REMOTE_MUTE, CarVolumeGroupEvent.EXTRA_INFO_MUTE_TOGGLED_BY_AUDIO_SYSTEM)142         REASONS_TO_EXTRA_INFO.put(Reasons.REMOTE_MUTE,
143                 CarVolumeGroupEvent.EXTRA_INFO_MUTE_TOGGLED_BY_AUDIO_SYSTEM);
REASONS_TO_EXTRA_INFO.put(Reasons.TCU_MUTE, CarVolumeGroupEvent.EXTRA_INFO_MUTE_TOGGLED_BY_EMERGENCY)144         REASONS_TO_EXTRA_INFO.put(Reasons.TCU_MUTE,
145                 CarVolumeGroupEvent.EXTRA_INFO_MUTE_TOGGLED_BY_EMERGENCY);
REASONS_TO_EXTRA_INFO.put(Reasons.ADAS_DUCKING, CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_EXTERNAL)146         REASONS_TO_EXTRA_INFO.put(Reasons.ADAS_DUCKING,
147                 CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_EXTERNAL);
REASONS_TO_EXTRA_INFO.put(Reasons.NAV_DUCKING, CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_NAVIGATION)148         REASONS_TO_EXTRA_INFO.put(Reasons.NAV_DUCKING,
149                 CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_NAVIGATION);
REASONS_TO_EXTRA_INFO.put(Reasons.PROJECTION_DUCKING, CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_PROJECTION)150         REASONS_TO_EXTRA_INFO.put(Reasons.PROJECTION_DUCKING,
151                 CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_PROJECTION);
REASONS_TO_EXTRA_INFO.put(Reasons.THERMAL_LIMITATION, CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_THERMAL)152         REASONS_TO_EXTRA_INFO.put(Reasons.THERMAL_LIMITATION,
153                 CarVolumeGroupEvent.EXTRA_INFO_TRANSIENT_ATTENUATION_THERMAL);
REASONS_TO_EXTRA_INFO.put(Reasons.SUSPEND_EXIT_VOL_LIMITATION, CarVolumeGroupEvent.EXTRA_INFO_ATTENUATION_ACTIVATION)154         REASONS_TO_EXTRA_INFO.put(Reasons.SUSPEND_EXIT_VOL_LIMITATION,
155                 CarVolumeGroupEvent.EXTRA_INFO_ATTENUATION_ACTIVATION);
REASONS_TO_EXTRA_INFO.put(Reasons.EXTERNAL_AMP_VOL_FEEDBACK, CarVolumeGroupEvent.EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM)156         REASONS_TO_EXTRA_INFO.put(Reasons.EXTERNAL_AMP_VOL_FEEDBACK,
157                 CarVolumeGroupEvent.EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM);
158     }
159 
convertReasonsToExtraInfo(List<Integer> reasons)160     static List<Integer> convertReasonsToExtraInfo(List<Integer> reasons) {
161         List<Integer> extraInfos = new ArrayList<>();
162         for (int index = 0; index < reasons.size(); index++) {
163             // cannot assume validaty of reason.
164             // also not all reasons are supported in extra-info
165             int extraInfo = REASONS_TO_EXTRA_INFO.get(reasons.get(index), INVALID_INFO);
166             if (extraInfo != INVALID_INFO) {
167                 extraInfos.add(extraInfo);
168             }
169         }
170         return extraInfos;
171     }
172 }
173