• 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 com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
20 import static com.android.car.audio.CarAudioService.SystemClockWrapper;
21 import static com.android.car.audio.CarAudioUtils.hasExpired;
22 
23 import android.annotation.IntDef;
24 import android.annotation.NonNull;
25 import android.media.AudioAttributes.AttributeUsage;
26 import android.media.AudioPlaybackConfiguration;
27 import android.telephony.Annotation.CallState;
28 import android.telephony.TelephonyManager;
29 import android.util.SparseIntArray;
30 
31 import com.android.car.CarLog;
32 import com.android.car.audio.CarAudioContext.AudioContext;
33 import com.android.internal.annotations.GuardedBy;
34 import com.android.internal.util.Preconditions;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.List;
39 import java.util.Objects;
40 import java.util.Set;
41 
42 /**
43  * CarVolume is responsible for determining which audio contexts to prioritize when adjusting volume
44  */
45 final class CarVolume {
46     private static final String TAG = CarLog.tagFor(CarVolume.class);
47     private static final int CONTEXT_HIGHEST_PRIORITY = 0;
48     private static final int CONTEXT_NOT_PRIORITIZED = -1;
49 
50     static final int VERSION_ONE = 1;
51     private static final int[] AUDIO_CONTEXT_VOLUME_PRIORITY_V1 = {
52             CarAudioContext.NAVIGATION,
53             CarAudioContext.CALL,
54             CarAudioContext.MUSIC,
55             CarAudioContext.ANNOUNCEMENT,
56             CarAudioContext.VOICE_COMMAND,
57             CarAudioContext.CALL_RING,
58             CarAudioContext.SYSTEM_SOUND,
59             CarAudioContext.SAFETY,
60             CarAudioContext.ALARM,
61             CarAudioContext.NOTIFICATION,
62             CarAudioContext.VEHICLE_STATUS,
63             CarAudioContext.EMERGENCY,
64             // CarAudioContext.INVALID is intentionally not prioritized as it is not routed by
65             // CarAudioService and is not expected to be used.
66     };
67 
68     static final int VERSION_TWO = 2;
69     private static final int[] AUDIO_CONTEXT_VOLUME_PRIORITY_V2 = {
70             CarAudioContext.CALL,
71             CarAudioContext.MUSIC,
72             CarAudioContext.ANNOUNCEMENT,
73             CarAudioContext.VOICE_COMMAND,
74     };
75 
76     private final SparseIntArray mVolumePriorityByAudioContext = new SparseIntArray();
77     private final SystemClockWrapper mClock;
78     private final Object mLock = new Object();
79     private final int mVolumeKeyEventTimeoutMs;
80     private final int mLowestPriority;
81     @GuardedBy("mLock")
82     @AudioContext private int mLastActiveContext;
83     @GuardedBy("mLock")
84     private long mLastActiveContextStartTime;
85 
86     /**
87      * Creates car volume for management of volume priority and last selected audio context.
88      * @param clockWrapper time keeper for expiration of last selected context.
89      * @param audioVolumeAdjustmentContextsVersion audio priority list version number, can be
90      *      any version defined in {@link CarVolumeListVersion}
91      * @param volumeKeyEventTimeoutMs timeout in ms used to measure expiration of last selected
92      *      context
93      */
CarVolume(@onNull SystemClockWrapper clockWrapper, @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion, int volumeKeyEventTimeoutMs)94     CarVolume(@NonNull SystemClockWrapper clockWrapper,
95             @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion,
96             int volumeKeyEventTimeoutMs) {
97         mClock = Objects.requireNonNull(clockWrapper, "Clock must not be null.");
98         mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs);
99         mLastActiveContext = CarAudioContext.INVALID;
100         mLastActiveContextStartTime = mClock.uptimeMillis();
101         @AudioContext int[] contextVolumePriority =
102                 getContextPriorityList(audioVolumeAdjustmentContextsVersion);
103 
104         for (int priority = CONTEXT_HIGHEST_PRIORITY;
105                 priority < contextVolumePriority.length; priority++) {
106             mVolumePriorityByAudioContext.append(contextVolumePriority[priority], priority);
107         }
108 
109         mLowestPriority = CONTEXT_HIGHEST_PRIORITY + mVolumePriorityByAudioContext.size();
110 
111     }
112 
getContextPriorityList(int audioVolumeAdjustmentContextsVersion)113     private static int[] getContextPriorityList(int audioVolumeAdjustmentContextsVersion) {
114         Preconditions.checkArgumentInRange(audioVolumeAdjustmentContextsVersion, 1, 2,
115                 "audioVolumeAdjustmentContextsVersion");
116         if (audioVolumeAdjustmentContextsVersion == VERSION_TWO) {
117             return AUDIO_CONTEXT_VOLUME_PRIORITY_V2;
118         }
119         return AUDIO_CONTEXT_VOLUME_PRIORITY_V1;
120     }
121 
122     /**
123      * @see {@link CarAudioService#resetSelectedVolumeContext()}
124      */
resetSelectedVolumeContext()125     public void resetSelectedVolumeContext() {
126         setAudioContextStillActive(CarAudioContext.INVALID);
127     }
128 
129     /**
130      * Finds a {@link AudioContext} that should be adjusted based on the current
131      * {@link AudioPlaybackConfiguration}s, {@link CallState}, and active HAL usages. If an active
132      * context is found it be will saved and retrieved later on.
133      */
getSuggestedAudioContextAndSaveIfFound( @onNull List<Integer> activePlaybackContexts, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages)134     @AudioContext int getSuggestedAudioContextAndSaveIfFound(
135             @NonNull List<Integer> activePlaybackContexts, @CallState int callState,
136             @NonNull @AttributeUsage int[] activeHalUsages) {
137 
138         int activeContext = getAudioContextStillActive();
139         if (activeContext != CarAudioContext.INVALID) {
140             setAudioContextStillActive(activeContext);
141             return activeContext;
142         }
143 
144         Set<Integer> activeContexts = getActiveContexts(activePlaybackContexts, callState,
145                 activeHalUsages);
146 
147 
148         @AudioContext int context =
149                 findActiveContextWithHighestPriority(activeContexts, mVolumePriorityByAudioContext);
150 
151         setAudioContextStillActive(context);
152 
153         return context;
154     }
155 
findActiveContextWithHighestPriority( Set<Integer> activeContexts, SparseIntArray contextPriorities)156     private @AudioContext int findActiveContextWithHighestPriority(
157             Set<Integer> activeContexts, SparseIntArray contextPriorities) {
158         int currentContext = DEFAULT_AUDIO_CONTEXT;
159         int currentPriority = mLowestPriority;
160 
161         for (@AudioContext int context : activeContexts) {
162             int priority = contextPriorities.get(context, CONTEXT_NOT_PRIORITIZED);
163             if (priority == CONTEXT_NOT_PRIORITIZED) {
164                 continue;
165             }
166 
167             if (priority < currentPriority) {
168                 currentContext = context;
169                 currentPriority = priority;
170                 // If the highest priority has been found, break early.
171                 if (currentPriority == CONTEXT_HIGHEST_PRIORITY) {
172                     break;
173                 }
174             }
175         }
176 
177         return currentContext;
178     }
179 
setAudioContextStillActive(@udioContext int context)180     private void setAudioContextStillActive(@AudioContext int context) {
181         synchronized (mLock) {
182             mLastActiveContext = context;
183             mLastActiveContextStartTime = mClock.uptimeMillis();
184         }
185     }
186 
isAnyContextActive(@onNull @udioContext int [] contexts, @NonNull List<Integer> activePlaybackContext, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages)187     public static boolean isAnyContextActive(@NonNull @AudioContext int [] contexts,
188             @NonNull List<Integer> activePlaybackContext, @CallState int callState,
189             @NonNull @AttributeUsage int[] activeHalUsages) {
190         Objects.nonNull(contexts);
191         Preconditions.checkArgument(contexts.length != 0,
192                 "contexts can not be empty.");
193         Set<Integer> activeContexts = getActiveContexts(activePlaybackContext,
194                 callState, activeHalUsages);
195         for (@AudioContext int context : contexts) {
196             if (activeContexts.contains(context)) {
197                 return true;
198             }
199         }
200         return false;
201     }
202 
getActiveContexts(@onNull List<Integer> activePlaybackContexts, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages)203     private static Set<Integer> getActiveContexts(@NonNull List<Integer> activePlaybackContexts,
204             @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) {
205         Objects.nonNull(activePlaybackContexts);
206         Objects.nonNull(activeHalUsages);
207 
208         Set<Integer> contexts = CarAudioContext.getUniqueContextsForUsages(activeHalUsages);
209 
210         switch (callState) {
211             case TelephonyManager.CALL_STATE_RINGING:
212                 contexts.add(CarAudioContext.CALL_RING);
213                 break;
214             case TelephonyManager.CALL_STATE_OFFHOOK:
215                 contexts.add(CarAudioContext.CALL);
216                 break;
217         }
218 
219         contexts.addAll(activePlaybackContexts);
220         return contexts;
221     }
222 
getAudioContextStillActive()223     private @AudioContext int getAudioContextStillActive() {
224         @AudioContext int context;
225         long contextStartTime;
226         synchronized (mLock) {
227             context = mLastActiveContext;
228             contextStartTime = mLastActiveContextStartTime;
229         }
230 
231         if (context == CarAudioContext.INVALID) {
232             return CarAudioContext.INVALID;
233         }
234 
235         if (hasExpired(contextStartTime, mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) {
236             return CarAudioContext.INVALID;
237         }
238 
239         return context;
240     }
241 
242     @IntDef({
243             VERSION_ONE,
244             VERSION_TWO
245     })
246     @Retention(RetentionPolicy.SOURCE)
247     public @interface CarVolumeListVersion {
248     }
249 }
250