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