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