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