/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.audio; import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT; import static com.android.car.audio.CarAudioService.SystemClockWrapper; import static com.android.car.audio.CarAudioUtils.hasExpired; import android.annotation.IntDef; import android.annotation.NonNull; import android.media.AudioAttributes.AttributeUsage; import android.media.AudioPlaybackConfiguration; import android.telephony.Annotation.CallState; import android.telephony.TelephonyManager; import android.util.SparseIntArray; import com.android.car.CarLog; import com.android.car.audio.CarAudioContext.AudioContext; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Objects; import java.util.Set; /** * CarVolume is responsible for determining which audio contexts to prioritize when adjusting volume */ final class CarVolume { private static final String TAG = CarLog.tagFor(CarVolume.class); private static final int CONTEXT_HIGHEST_PRIORITY = 0; private static final int CONTEXT_NOT_PRIORITIZED = -1; static final int VERSION_ONE = 1; private static final int[] AUDIO_CONTEXT_VOLUME_PRIORITY_V1 = { CarAudioContext.NAVIGATION, CarAudioContext.CALL, CarAudioContext.MUSIC, CarAudioContext.ANNOUNCEMENT, CarAudioContext.VOICE_COMMAND, CarAudioContext.CALL_RING, CarAudioContext.SYSTEM_SOUND, CarAudioContext.SAFETY, CarAudioContext.ALARM, CarAudioContext.NOTIFICATION, CarAudioContext.VEHICLE_STATUS, CarAudioContext.EMERGENCY, // CarAudioContext.INVALID is intentionally not prioritized as it is not routed by // CarAudioService and is not expected to be used. }; static final int VERSION_TWO = 2; private static final int[] AUDIO_CONTEXT_VOLUME_PRIORITY_V2 = { CarAudioContext.CALL, CarAudioContext.MUSIC, CarAudioContext.ANNOUNCEMENT, CarAudioContext.VOICE_COMMAND, }; private final SparseIntArray mVolumePriorityByAudioContext = new SparseIntArray(); private final SystemClockWrapper mClock; private final Object mLock = new Object(); private final int mVolumeKeyEventTimeoutMs; private final int mLowestPriority; @GuardedBy("mLock") @AudioContext private int mLastActiveContext; @GuardedBy("mLock") private long mLastActiveContextStartTime; /** * Creates car volume for management of volume priority and last selected audio context. * @param clockWrapper time keeper for expiration of last selected context. * @param audioVolumeAdjustmentContextsVersion audio priority list version number, can be * any version defined in {@link CarVolumeListVersion} * @param volumeKeyEventTimeoutMs timeout in ms used to measure expiration of last selected * context */ CarVolume(@NonNull SystemClockWrapper clockWrapper, @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion, int volumeKeyEventTimeoutMs) { mClock = Objects.requireNonNull(clockWrapper, "Clock must not be null."); mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs); mLastActiveContext = CarAudioContext.INVALID; mLastActiveContextStartTime = mClock.uptimeMillis(); @AudioContext int[] contextVolumePriority = getContextPriorityList(audioVolumeAdjustmentContextsVersion); for (int priority = CONTEXT_HIGHEST_PRIORITY; priority < contextVolumePriority.length; priority++) { mVolumePriorityByAudioContext.append(contextVolumePriority[priority], priority); } mLowestPriority = CONTEXT_HIGHEST_PRIORITY + mVolumePriorityByAudioContext.size(); } private static int[] getContextPriorityList(int audioVolumeAdjustmentContextsVersion) { Preconditions.checkArgumentInRange(audioVolumeAdjustmentContextsVersion, 1, 2, "audioVolumeAdjustmentContextsVersion"); if (audioVolumeAdjustmentContextsVersion == VERSION_TWO) { return AUDIO_CONTEXT_VOLUME_PRIORITY_V2; } return AUDIO_CONTEXT_VOLUME_PRIORITY_V1; } /** * @see {@link CarAudioService#resetSelectedVolumeContext()} */ public void resetSelectedVolumeContext() { setAudioContextStillActive(CarAudioContext.INVALID); } /** * Finds a {@link AudioContext} that should be adjusted based on the current * {@link AudioPlaybackConfiguration}s, {@link CallState}, and active HAL usages. If an active * context is found it be will saved and retrieved later on. */ @AudioContext int getSuggestedAudioContextAndSaveIfFound( @NonNull List activePlaybackContexts, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) { int activeContext = getAudioContextStillActive(); if (activeContext != CarAudioContext.INVALID) { setAudioContextStillActive(activeContext); return activeContext; } Set activeContexts = getActiveContexts(activePlaybackContexts, callState, activeHalUsages); @AudioContext int context = findActiveContextWithHighestPriority(activeContexts, mVolumePriorityByAudioContext); setAudioContextStillActive(context); return context; } private @AudioContext int findActiveContextWithHighestPriority( Set activeContexts, SparseIntArray contextPriorities) { int currentContext = DEFAULT_AUDIO_CONTEXT; int currentPriority = mLowestPriority; for (@AudioContext int context : activeContexts) { int priority = contextPriorities.get(context, CONTEXT_NOT_PRIORITIZED); if (priority == CONTEXT_NOT_PRIORITIZED) { continue; } if (priority < currentPriority) { currentContext = context; currentPriority = priority; // If the highest priority has been found, break early. if (currentPriority == CONTEXT_HIGHEST_PRIORITY) { break; } } } return currentContext; } private void setAudioContextStillActive(@AudioContext int context) { synchronized (mLock) { mLastActiveContext = context; mLastActiveContextStartTime = mClock.uptimeMillis(); } } public static boolean isAnyContextActive(@NonNull @AudioContext int [] contexts, @NonNull List activePlaybackContext, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) { Objects.nonNull(contexts); Preconditions.checkArgument(contexts.length != 0, "contexts can not be empty."); Set activeContexts = getActiveContexts(activePlaybackContext, callState, activeHalUsages); for (@AudioContext int context : contexts) { if (activeContexts.contains(context)) { return true; } } return false; } private static Set getActiveContexts(@NonNull List activePlaybackContexts, @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) { Objects.nonNull(activePlaybackContexts); Objects.nonNull(activeHalUsages); Set contexts = CarAudioContext.getUniqueContextsForUsages(activeHalUsages); switch (callState) { case TelephonyManager.CALL_STATE_RINGING: contexts.add(CarAudioContext.CALL_RING); break; case TelephonyManager.CALL_STATE_OFFHOOK: contexts.add(CarAudioContext.CALL); break; } contexts.addAll(activePlaybackContexts); return contexts; } private @AudioContext int getAudioContextStillActive() { @AudioContext int context; long contextStartTime; synchronized (mLock) { context = mLastActiveContext; contextStartTime = mLastActiveContextStartTime; } if (context == CarAudioContext.INVALID) { return CarAudioContext.INVALID; } if (hasExpired(contextStartTime, mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) { return CarAudioContext.INVALID; } return context; } @IntDef({ VERSION_ONE, VERSION_TWO }) @Retention(RetentionPolicy.SOURCE) public @interface CarVolumeListVersion { } }