1 /*
2  * Copyright (C) 2020 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 static android.media.AudioAttributes.USAGE_ALARM;
20 import static android.media.AudioAttributes.USAGE_ANNOUNCEMENT;
21 import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
22 import static android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
23 import static android.media.AudioAttributes.USAGE_ASSISTANT;
24 import static android.media.AudioAttributes.USAGE_EMERGENCY;
25 import static android.media.AudioAttributes.USAGE_MEDIA;
26 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
27 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
28 import static android.media.AudioAttributes.USAGE_SAFETY;
29 import static android.media.AudioAttributes.USAGE_VEHICLE_STATUS;
30 import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
31 import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
32 import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
33 
34 import static com.android.car.audio.CarAudioService.CAR_DEFAULT_AUDIO_ATTRIBUTE;
35 import static com.android.car.audio.CarAudioService.SystemClockWrapper;
36 import static com.android.car.audio.CarAudioUtils.hasExpired;
37 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
38 
39 import android.annotation.IntDef;
40 import android.media.AudioAttributes;
41 import android.media.AudioPlaybackConfiguration;
42 import android.util.ArraySet;
43 import android.util.SparseIntArray;
44 import android.util.proto.ProtoOutputStream;
45 
46 import com.android.car.CarLog;
47 import com.android.car.CarServiceUtils;
48 import com.android.car.audio.CarAudioContext.AudioContext;
49 import com.android.car.audio.CarAudioDumpProto.CarAudioContextInfoProto;
50 import com.android.car.audio.CarAudioDumpProto.CarVolumeProto;
51 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
52 import com.android.car.internal.util.IndentingPrintWriter;
53 import com.android.internal.annotations.GuardedBy;
54 import com.android.internal.util.Preconditions;
55 
56 import java.lang.annotation.Retention;
57 import java.lang.annotation.RetentionPolicy;
58 import java.util.ArrayList;
59 import java.util.Comparator;
60 import java.util.List;
61 import java.util.Objects;
62 import java.util.Set;
63 
64 /**
65  * CarVolume is responsible for determining which audio contexts to prioritize when adjusting volume
66  */
67 final class CarVolume {
68     private static final String TAG = CarLog.tagFor(CarVolume.class);
69     private static final int CONTEXT_HIGHEST_PRIORITY = 0;
70     private static final int CONTEXT_NOT_PRIORITIZED = -1;
71 
72     static final int VERSION_ONE = 1;
73     private static final List<AudioAttributes> AUDIO_ATTRIBUTE_VOLUME_PRIORITY_V1 = List.of(
74             // CarAudioContext.getInvalidContext() is intentionally not prioritized
75             // as it is not routed by CarAudioService and is not expected to be used.
76             CarAudioContext.getAudioAttributeFromUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE),
77             CarAudioContext.getAudioAttributeFromUsage(USAGE_VOICE_COMMUNICATION),
78             CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA),
79             CarAudioContext.getAudioAttributeFromUsage(USAGE_ANNOUNCEMENT),
80             CarAudioContext.getAudioAttributeFromUsage(USAGE_ASSISTANT),
81             CarAudioContext.getAudioAttributeFromUsage(USAGE_NOTIFICATION_RINGTONE),
82             CarAudioContext.getAudioAttributeFromUsage(USAGE_ASSISTANCE_SONIFICATION),
83             CarAudioContext.getAudioAttributeFromUsage(USAGE_SAFETY),
84             CarAudioContext.getAudioAttributeFromUsage(USAGE_ALARM),
85             CarAudioContext.getAudioAttributeFromUsage(USAGE_NOTIFICATION),
86             CarAudioContext.getAudioAttributeFromUsage(USAGE_VEHICLE_STATUS),
87             CarAudioContext.getAudioAttributeFromUsage(USAGE_EMERGENCY)
88     );
89 
90     static final int VERSION_TWO = 2;
91     private static final List<AudioAttributes> AUDIO_ATTRIBUTE_VOLUME_PRIORITY_V2 = List.of(
92             CarAudioContext.getAudioAttributeFromUsage(USAGE_VOICE_COMMUNICATION),
93             CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA),
94             CarAudioContext.getAudioAttributeFromUsage(USAGE_ANNOUNCEMENT),
95             CarAudioContext.getAudioAttributeFromUsage(USAGE_ASSISTANT)
96     );
97 
98     private final SparseIntArray mVolumePriorityByAudioContext = new SparseIntArray();
99     private final SystemClockWrapper mClock;
100     private final Object mLock = new Object();
101     private final int mVolumeKeyEventTimeoutMs;
102     private final int mLowestPriority;
103     private final CarAudioContext mCarAudioContext;
104     private final int mAudioVolumeAdjustmentContextsVersion;
105     @GuardedBy("mLock")
106     @AudioContext private int mLastActiveContext;
107     @GuardedBy("mLock")
108     private long mLastActiveContextStartTime;
109 
110     /**
111      * Creates car volume for management of volume priority and last selected audio context.
112      *
113      * @param carAudioContext car audio context for the logical grouping of audio usages
114      * @param clockWrapper time keeper for expiration of last selected context.
115      * @param audioVolumeAdjustmentContextsVersion audio priority list version number, can be
116      *      any version defined in {@link CarVolumeListVersion}
117      * @param volumeKeyEventTimeoutMs timeout in ms used to measure expiration of last selected
118      *      context
119      */
CarVolume(CarAudioContext carAudioContext, SystemClockWrapper clockWrapper, @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion, int volumeKeyEventTimeoutMs)120     CarVolume(CarAudioContext carAudioContext, SystemClockWrapper clockWrapper,
121             @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion,
122             int volumeKeyEventTimeoutMs) {
123         mCarAudioContext = Objects.requireNonNull(carAudioContext,
124                 "Car audio context must not be null");
125         mClock = Objects.requireNonNull(clockWrapper, "Clock must not be null.");
126         mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs);
127         mLastActiveContext = CarAudioContext.getInvalidContext();
128         mLastActiveContextStartTime = mClock.uptimeMillis();
129         @AudioContext int[] contextVolumePriority =
130                 getContextPriorityList(audioVolumeAdjustmentContextsVersion);
131 
132         for (int priority = CONTEXT_HIGHEST_PRIORITY;
133                 priority < contextVolumePriority.length; priority++) {
134             mVolumePriorityByAudioContext.append(contextVolumePriority[priority], priority);
135         }
136 
137         mLowestPriority = CONTEXT_HIGHEST_PRIORITY + mVolumePriorityByAudioContext.size();
138         mAudioVolumeAdjustmentContextsVersion = audioVolumeAdjustmentContextsVersion;
139 
140     }
141 
getContextPriorityList(int audioVolumeAdjustmentContextsVersion)142     private int[] getContextPriorityList(int audioVolumeAdjustmentContextsVersion) {
143         Preconditions.checkArgumentInRange(audioVolumeAdjustmentContextsVersion, 1, 2,
144                 "audioVolumeAdjustmentContextsVersion");
145         if (audioVolumeAdjustmentContextsVersion == VERSION_TWO) {
146             return convertAttributesToContexts(AUDIO_ATTRIBUTE_VOLUME_PRIORITY_V2);
147         }
148         return convertAttributesToContexts(AUDIO_ATTRIBUTE_VOLUME_PRIORITY_V1);
149     }
150 
convertAttributesToContexts(List<AudioAttributes> audioAttributesPriorities)151     private int[] convertAttributesToContexts(List<AudioAttributes> audioAttributesPriorities) {
152         ArraySet<Integer> contexts = new ArraySet<>();
153         List<Integer> contextByPriority = new ArrayList<>();
154         for (int index = 0; index < audioAttributesPriorities.size(); index++) {
155             int context = mCarAudioContext.getContextForAudioAttribute(
156                     audioAttributesPriorities.get(index));
157             if (contexts.contains(context)) {
158                 // Audio attribute was already group into another context,
159                 // use the higher priority if so.
160                 continue;
161             }
162             contexts.add(context);
163             contextByPriority.add(context);
164         }
165 
166         return CarServiceUtils.toIntArray(contextByPriority);
167     }
168 
convertAttributesToContextsSet(List<AudioAttributes> audioAttributesPriorities)169     private ArraySet<Integer> convertAttributesToContextsSet(List<AudioAttributes>
170             audioAttributesPriorities) {
171         ArraySet<Integer> contexts = new ArraySet<>();
172         for (int index = 0; index < audioAttributesPriorities.size(); index++) {
173             contexts.add(mCarAudioContext.getContextForAudioAttribute(
174                     audioAttributesPriorities.get(index)));
175         }
176         return contexts;
177     }
178 
179     /**
180      * @see CarAudioService#resetSelectedVolumeContext()
181      */
resetSelectedVolumeContext()182     public void resetSelectedVolumeContext() {
183         setAudioContextStillActive(CarAudioContext.getInvalidContext());
184     }
185 
186     /**
187      * Finds an active {@link AudioContext} that should be adjusted based on the current
188      * {@link AudioPlaybackConfiguration}s,
189      * {@code callState} (can be {@code CALL_STATE_OFFHOOK}, {@code CALL_STATE_RINGING}
190      * or {@code CALL_STATE_IDLE}). {@code callState} is used to determined if the call context
191      * or phone ringer context are active.
192      *
193      * <p> Note that if an active context is found it be will saved and retrieved later on.
194      */
getSuggestedAudioContextAndSaveIfFound( List<AudioAttributes> activePlaybackAttributes, int callState, List<AudioAttributes> activeHalAttributes, List<AudioAttributes> inactiveAudioAttributes)195     @AudioContext int getSuggestedAudioContextAndSaveIfFound(
196             List<AudioAttributes> activePlaybackAttributes, int callState,
197             List<AudioAttributes> activeHalAttributes,
198             List<AudioAttributes> inactiveAudioAttributes) {
199 
200         int activeContext = getAudioContextStillActive();
201         if (!CarAudioContext.isInvalidContextId(activeContext)) {
202             setAudioContextStillActive(activeContext);
203             return activeContext;
204         }
205 
206         ArraySet<AudioAttributes> activeAttributes =
207                 getActiveAttributes(activePlaybackAttributes, callState, activeHalAttributes);
208 
209         @AudioContext int context = findActiveContextWithHighestPriority(activeAttributes,
210                         mVolumePriorityByAudioContext, inactiveAudioAttributes);
211 
212         setAudioContextStillActive(context);
213 
214         return context;
215     }
216 
findActiveContextWithHighestPriority( ArraySet<AudioAttributes> activeAttributes, SparseIntArray contextPriorities, List<AudioAttributes> inactiveAudioAttributes)217     private @AudioContext int findActiveContextWithHighestPriority(
218             ArraySet<AudioAttributes> activeAttributes, SparseIntArray contextPriorities,
219             List<AudioAttributes> inactiveAudioAttributes) {
220         int currentContext = mCarAudioContext.getContextForAttributes(
221                 CAR_DEFAULT_AUDIO_ATTRIBUTE);
222         int currentPriority = mLowestPriority;
223 
224         ArraySet<Integer> inactiveContexts =
225                 convertAttributesToContextsSet(inactiveAudioAttributes);
226 
227         for (int index = 0; index < activeAttributes.size(); index++) {
228             @AudioContext int context = mCarAudioContext.getContextForAudioAttribute(
229                     activeAttributes.valueAt(index));
230 
231             if (inactiveContexts.contains(context)) {
232                 continue;
233             }
234             int priority = contextPriorities.get(context, CONTEXT_NOT_PRIORITIZED);
235             if (priority == CONTEXT_NOT_PRIORITIZED) {
236                 continue;
237             }
238 
239             if (priority < currentPriority) {
240                 currentContext = context;
241                 currentPriority = priority;
242                 // If the highest priority has been found, break early.
243                 if (currentPriority == CONTEXT_HIGHEST_PRIORITY) {
244                     break;
245                 }
246             }
247         }
248 
249         return !inactiveContexts.contains(currentContext) ? currentContext :
250                 getNextBestDefaultContext(inactiveContexts);
251     }
252 
getNextBestDefaultContext(ArraySet<Integer> inactiveContexts)253     private int getNextBestDefaultContext(ArraySet<Integer> inactiveContexts) {
254         int[] contextVolumePriority = getContextPriorityList(mAudioVolumeAdjustmentContextsVersion);
255         for (int c = 0; c < contextVolumePriority.length; c++) {
256             int context = contextVolumePriority[c];
257             if (inactiveContexts.contains(context)) {
258                 continue;
259             }
260             return context;
261         }
262         return CarAudioContext.getInvalidContext();
263     }
264 
setAudioContextStillActive(@udioContext int context)265     private void setAudioContextStillActive(@AudioContext int context) {
266         synchronized (mLock) {
267             mLastActiveContext = context;
268             mLastActiveContextStartTime = mClock.uptimeMillis();
269         }
270     }
271 
isAnyContextActive(@udioContext int [] contexts, List<AudioAttributes> activePlaybackContext, int callState, List<AudioAttributes> activeHalAudioAttributes)272     boolean isAnyContextActive(@AudioContext int [] contexts,
273             List<AudioAttributes> activePlaybackContext, int callState,
274             List<AudioAttributes> activeHalAudioAttributes) {
275         Objects.requireNonNull(contexts, "Contexts can not be null");
276         Preconditions.checkArgument(contexts.length != 0, "Contexts can not be empty");
277         Objects.requireNonNull(activeHalAudioAttributes, "Audio attributes can not be null");
278 
279         ArraySet<AudioAttributes> activeAttributes = getActiveAttributes(activePlaybackContext,
280                 callState, activeHalAudioAttributes);
281 
282         Set<Integer> activeContexts = new ArraySet<>(activeAttributes.size());
283 
284         for (int index = 0; index < activeAttributes.size(); index++) {
285             activeContexts.add(mCarAudioContext
286                     .getContextForAttributes(activeAttributes.valueAt(index)));
287         }
288 
289         for (int index = 0; index < contexts.length; index++) {
290             if (activeContexts.contains(contexts[index])) {
291                 return true;
292             }
293         }
294 
295         return false;
296     }
297 
getActiveAttributes( List<AudioAttributes> activeAttributes, int callState, List<AudioAttributes> activeHalAudioAttributes)298     private static ArraySet<AudioAttributes> getActiveAttributes(
299             List<AudioAttributes> activeAttributes, int callState,
300             List<AudioAttributes> activeHalAudioAttributes) {
301         Objects.requireNonNull(activeAttributes, "Playback audio attributes can not be null");
302         Objects.requireNonNull(activeHalAudioAttributes, "Active HAL contexts can not be null");
303 
304         ArraySet<AudioAttributes> attributes = new ArraySet<>(activeHalAudioAttributes);
305 
306         switch (callState) {
307             case CALL_STATE_RINGING:
308                 attributes.add(CarAudioContext
309                         .getAudioAttributeFromUsage(USAGE_NOTIFICATION_RINGTONE));
310                 break;
311             case CALL_STATE_OFFHOOK:
312                 attributes.add(CarAudioContext
313                         .getAudioAttributeFromUsage(USAGE_VOICE_COMMUNICATION));
314                 break;
315             default:
316                 break;
317         }
318 
319         attributes.addAll(activeAttributes);
320         return attributes;
321     }
322 
getAudioContextStillActive()323     private @AudioContext int getAudioContextStillActive() {
324         @AudioContext int context;
325         long contextStartTime;
326         synchronized (mLock) {
327             context = mLastActiveContext;
328             contextStartTime = mLastActiveContextStartTime;
329         }
330 
331         if (CarAudioContext.isInvalidContextId(context)) {
332             return CarAudioContext.getInvalidContext();
333         }
334 
335         if (hasExpired(contextStartTime, mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) {
336             return CarAudioContext.getInvalidContext();
337         }
338 
339         return context;
340     }
341 
342     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)343     void dump(IndentingPrintWriter writer) {
344         writer.println("CarVolume");
345         writer.increaseIndent();
346 
347         writer.printf("Volume priority list version %d\n",
348                 mAudioVolumeAdjustmentContextsVersion);
349         writer.printf("Volume key event timeout %d ms\n", mVolumeKeyEventTimeoutMs);
350         writer.println("Car audio contexts priorities");
351 
352         writer.increaseIndent();
353         dumpSortedContexts(writer);
354         writer.decreaseIndent();
355 
356         writer.decreaseIndent();
357     }
358 
359     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpSortedContexts(IndentingPrintWriter writer)360     private void dumpSortedContexts(IndentingPrintWriter writer) {
361         List<Integer> sortedContexts = getSortedContexts();
362 
363         for (int index = 0; index < sortedContexts.size(); index++) {
364             int contextId = sortedContexts.get(index);
365             int priority = mVolumePriorityByAudioContext.get(contextId);
366             writer.printf("Car audio context %s[id=%d] priority %d\n",
367                     mCarAudioContext.toString(contextId), contextId, priority);
368             AudioAttributes[] attributes =
369                     mCarAudioContext.getAudioAttributesForContext(contextId);
370             writer.increaseIndent();
371             for (int counter = 0; counter < attributes.length; counter++) {
372                 writer.printf("Attribute: %s\n", attributes[counter]);
373             }
374             writer.decreaseIndent();
375         }
376     }
377 
378     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)379     void dumpProto(ProtoOutputStream proto) {
380         long carVolumeToken = proto.start(CarAudioDumpProto.CAR_VOLUME);
381         proto.write(CarVolumeProto.AUDIO_VOLUME_ADJUSTMENT_CONTEXTS_VERSION,
382                 mAudioVolumeAdjustmentContextsVersion);
383         proto.write(CarVolumeProto.VOLUME_KEY_EVENT_TIMEOUT_MS, mVolumeKeyEventTimeoutMs);
384         dumpProtoSortedContexts(proto);
385         proto.end(carVolumeToken);
386     }
387 
388     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProtoSortedContexts(ProtoOutputStream proto)389     private void dumpProtoSortedContexts(ProtoOutputStream proto) {
390         List<Integer> sortedContexts = getSortedContexts();
391 
392         for (int index = 0; index < sortedContexts.size(); index++) {
393             int contextId = sortedContexts.get(index);
394             int priority = mVolumePriorityByAudioContext.get(contextId);
395             long audioContextPrioritiesToken = proto.start(CarVolumeProto.AUDIO_CONTEXT_PRIORITIES);
396 
397             long audioContextToken = proto.start(CarVolumeProto.CarAudioContextPriority.CONTEXTS);
398             proto.write(CarAudioContextInfoProto.NAME,
399                     mCarAudioContext.toString(contextId));
400             proto.write(CarAudioContextInfoProto.ID, contextId);
401             AudioAttributes[] attributes =
402                     mCarAudioContext.getAudioAttributesForContext(contextId);
403             for (int counter = 0; counter < attributes.length; counter++) {
404                 CarAudioContextInfo.dumpCarAudioAttributesProto(attributes[counter],
405                         CarAudioContextInfoProto.ATTRIBUTES, proto);
406             }
407             proto.end(audioContextToken);
408 
409             proto.write(CarVolumeProto.CarAudioContextPriority.PRIORITY, priority);
410             proto.end(audioContextPrioritiesToken);
411         }
412     }
413 
414     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
getSortedContexts()415     private List<Integer> getSortedContexts() {
416         List<Integer> sortedContexts = new ArrayList<>(mVolumePriorityByAudioContext.size());
417         for (int index = 0; index < mVolumePriorityByAudioContext.size(); index++) {
418             int contextId = mVolumePriorityByAudioContext.keyAt(index);
419             sortedContexts.add(contextId);
420         }
421         sortedContexts.sort(Comparator.comparingInt(mVolumePriorityByAudioContext::get));
422         return sortedContexts;
423     }
424 
425     @IntDef({
426             VERSION_ONE,
427             VERSION_TWO
428     })
429     @Retention(RetentionPolicy.SOURCE)
430     public @interface CarVolumeListVersion {
431     }
432 }
433