blob: 743e9324c913dbb9cbc2f67319038e54578151f9 [file] [log] [blame]
/*
* 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<Integer> activePlaybackContexts, @CallState int callState,
@NonNull @AttributeUsage int[] activeHalUsages) {
int activeContext = getAudioContextStillActive();
if (activeContext != CarAudioContext.INVALID) {
setAudioContextStillActive(activeContext);
return activeContext;
}
Set<Integer> activeContexts = getActiveContexts(activePlaybackContexts, callState,
activeHalUsages);
@AudioContext int context =
findActiveContextWithHighestPriority(activeContexts, mVolumePriorityByAudioContext);
setAudioContextStillActive(context);
return context;
}
private @AudioContext int findActiveContextWithHighestPriority(
Set<Integer> 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<Integer> activePlaybackContext, @CallState int callState,
@NonNull @AttributeUsage int[] activeHalUsages) {
Objects.nonNull(contexts);
Preconditions.checkArgument(contexts.length != 0,
"contexts can not be empty.");
Set<Integer> activeContexts = getActiveContexts(activePlaybackContext,
callState, activeHalUsages);
for (@AudioContext int context : contexts) {
if (activeContexts.contains(context)) {
return true;
}
}
return false;
}
private static Set<Integer> getActiveContexts(@NonNull List<Integer> activePlaybackContexts,
@CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) {
Objects.nonNull(activePlaybackContexts);
Objects.nonNull(activeHalUsages);
Set<Integer> 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 {
}
}