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