Updating getSuggestedAudioUsage
- CarVolume now is responsible for making suggestion on the volume group
to change
- Contexts to consider are based on active playback configurations and
telephony state
Bug: 154149359
Test: atest CarVolumeTest com.android.car.audio
Change-Id: Id16a4c5318b2a89a74f1245fb9b24ec3d3938d28
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 90c9b7b..6cf9cd8 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -52,6 +52,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
+import android.telephony.Annotation.CallState;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -140,13 +141,21 @@
new AudioPolicy.AudioPolicyVolumeCallback() {
@Override
public void onVolumeAdjustment(int adjustment) {
- final int usage = getSuggestedAudioUsage();
- Log.v(CarLog.TAG_AUDIO,
- "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
- + " suggested usage: " + AudioAttributes.usageToString(usage));
- // TODO: Pass zone id into this callback.
- final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
- final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
+ int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+ @AudioContext int suggestedContext = getSuggestedAudioContext();
+
+ int groupId;
+ synchronized (mImplLock) {
+ groupId = getVolumeGroupIdForAudioContextLocked(zoneId, suggestedContext);
+ }
+
+ if (Log.isLoggable(CarLog.TAG_AUDIO, Log.VERBOSE)) {
+ Log.v(CarLog.TAG_AUDIO, "onVolumeAdjustment: "
+ + AudioManager.adjustToString(adjustment) + " suggested audio context: "
+ + CarAudioContext.toString(suggestedContext) + " suggested volume group: "
+ + groupId);
+ }
+
final int currentVolume = getGroupVolume(zoneId, groupId);
final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
switch (adjustment) {
@@ -792,17 +801,22 @@
Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
"zoneId out of range: " + zoneId);
- CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
- for (int i = 0; i < groups.length; i++) {
- int[] contexts = groups[i].getContexts();
- for (int context : contexts) {
- if (CarAudioContext.getContextForUsage(usage) == context) {
- return i;
- }
+ @AudioContext int audioContext = CarAudioContext.getContextForUsage(usage);
+ return getVolumeGroupIdForAudioContextLocked(zoneId, audioContext);
+ }
+ }
+
+ private int getVolumeGroupIdForAudioContextLocked(int zoneId, @AudioContext int audioContext) {
+ CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
+ for (int i = 0; i < groups.length; i++) {
+ int[] groupAudioContexts = groups[i].getContexts();
+ for (int groupAudioContext : groupAudioContexts) {
+ if (audioContext == groupAudioContext) {
+ return i;
}
}
- return INVALID_VOLUME_GROUP_ID;
}
+ return INVALID_VOLUME_GROUP_ID;
}
@Override
@@ -1113,29 +1127,11 @@
return group.getAudioDevicePortForContext(CarAudioContext.getContextForUsage(usage));
}
- /**
- * @return The suggested {@link AudioAttributes} usage to which the volume key events apply
- */
- private @AudioAttributes.AttributeUsage int getSuggestedAudioUsage() {
- int callState = mTelephonyManager.getCallState();
- if (callState == TelephonyManager.CALL_STATE_RINGING) {
- return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
- } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) {
- return AudioAttributes.USAGE_VOICE_COMMUNICATION;
- } else {
- List<AudioPlaybackConfiguration> playbacks = mAudioManager
- .getActivePlaybackConfigurations()
- .stream()
- .filter(AudioPlaybackConfiguration::isActive)
- .collect(Collectors.toList());
- if (!playbacks.isEmpty()) {
- // Get audio usage from active playbacks if there is any, last one if multiple
- return playbacks.get(playbacks.size() - 1).getAudioAttributes().getSystemUsage();
- } else {
- // TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
- return DEFAULT_AUDIO_USAGE;
- }
- }
+ private @AudioContext int getSuggestedAudioContext() {
+ @CallState int callState = mTelephonyManager.getCallState();
+ List<AudioPlaybackConfiguration> configurations =
+ mAudioManager.getActivePlaybackConfigurations();
+ return CarVolume.getSuggestedAudioContext(configurations, callState);
}
/**
@@ -1144,7 +1140,7 @@
* @return volume group id mapped from stream type
*/
private int getVolumeGroupIdForStreamType(int streamType) {
- int groupId = -1;
+ int groupId = INVALID_VOLUME_GROUP_ID;
for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
groupId = i;
diff --git a/service/src/com/android/car/audio/CarVolume.java b/service/src/com/android/car/audio/CarVolume.java
new file mode 100644
index 0000000..39f2c59
--- /dev/null
+++ b/service/src/com/android/car/audio/CarVolume.java
@@ -0,0 +1,105 @@
+/*
+ * 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 android.media.AudioAttributes;
+import android.media.AudioAttributes.AttributeUsage;
+import android.media.AudioPlaybackConfiguration;
+import android.telephony.Annotation.CallState;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.car.audio.CarAudioContext.AudioContext;
+
+import java.util.List;
+
+/**
+ * CarVolume is responsible for determining which audio contexts to prioritize when adjusting volume
+ */
+final class CarVolume {
+ private static final String TAG = CarVolume.class.getSimpleName();
+ private static final int CONTEXT_NOT_PRIORITIZED = -1;
+
+ private static final int[] AUDIO_CONTEXT_VOLUME_PRIORITY = {
+ 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.
+ };
+
+ private static final SparseIntArray VOLUME_PRIORITY_BY_AUDIO_CONTEXT = new SparseIntArray();
+
+ static {
+ for (int priority = 0; priority < AUDIO_CONTEXT_VOLUME_PRIORITY.length; priority++) {
+ VOLUME_PRIORITY_BY_AUDIO_CONTEXT.append(AUDIO_CONTEXT_VOLUME_PRIORITY[priority],
+ priority);
+ }
+ }
+
+ /**
+ * Suggests a {@link AudioContext} that should be adjusted based on the current
+ * {@link AudioPlaybackConfiguration}s and {@link CallState}.
+ */
+ static @AudioContext int getSuggestedAudioContext(
+ List<AudioPlaybackConfiguration> configurations, @CallState int callState) {
+ int currentContext = DEFAULT_AUDIO_CONTEXT;
+ int currentPriority = AUDIO_CONTEXT_VOLUME_PRIORITY.length;
+
+ if (callState == TelephonyManager.CALL_STATE_RINGING) {
+ currentContext = CarAudioContext.CALL_RING;
+ currentPriority = VOLUME_PRIORITY_BY_AUDIO_CONTEXT.get(CarAudioContext.CALL_RING);
+ } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) {
+ currentContext = CarAudioContext.CALL;
+ currentPriority = VOLUME_PRIORITY_BY_AUDIO_CONTEXT.get(CarAudioContext.CALL);
+ }
+
+ for (AudioPlaybackConfiguration configuration : configurations) {
+ if (!configuration.isActive()) {
+ continue;
+ }
+
+ @AttributeUsage int usage = configuration.getAudioAttributes().getSystemUsage();
+ @AudioContext int context = CarAudioContext.getContextForUsage(usage);
+ int priority = VOLUME_PRIORITY_BY_AUDIO_CONTEXT.get(context, CONTEXT_NOT_PRIORITIZED);
+ if (priority == CONTEXT_NOT_PRIORITIZED) {
+ Log.w(TAG, "Usage " + AudioAttributes.usageToString(usage) + " mapped to context "
+ + CarAudioContext.toString(context) + " which is not prioritized");
+ continue;
+ }
+
+ if (priority < currentPriority) {
+ currentContext = context;
+ currentPriority = priority;
+ }
+ }
+
+ return currentContext;
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java
new file mode 100644
index 0000000..06d2299
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.media.AudioAttributes.USAGE_ALARM;
+import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+import static android.media.AudioAttributes.USAGE_MEDIA;
+import static android.media.AudioAttributes.USAGE_NOTIFICATION;
+import static android.media.AudioAttributes.USAGE_VIRTUAL_SOURCE;
+import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
+import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
+import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
+import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
+
+import static com.android.car.audio.CarAudioContext.ALARM;
+import static com.android.car.audio.CarAudioContext.CALL;
+import static com.android.car.audio.CarAudioContext.CALL_RING;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioAttributes;
+import android.media.AudioAttributes.AttributeUsage;
+import android.media.AudioPlaybackConfiguration;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.audio.CarAudioContext.AudioContext;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class CarVolumeTest {
+ @Test
+ public void getSuggestedAudioContext_withNoConfigurationsAndIdleTelephony_returnsDefault() {
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(new ArrayList<>(),
+ CALL_STATE_IDLE);
+
+ assertThat(suggestedContext).isEqualTo(CarAudioService.DEFAULT_AUDIO_CONTEXT);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withOneConfiguration_returnsAssociatedContext() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_ALARM).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_IDLE);
+
+ assertThat(suggestedContext).isEqualTo(ALARM);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withCallStateOffHook_returnsCallContext() {
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(new ArrayList<>(),
+ CALL_STATE_OFFHOOK);
+
+ assertThat(suggestedContext).isEqualTo(CALL);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withCallStateRinging_returnsCallRingContext() {
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(new ArrayList<>(),
+ CALL_STATE_RINGING);
+
+ assertThat(suggestedContext).isEqualTo(CALL_RING);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withConfigurations_returnsHighestPriorityContext() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_ALARM).build(),
+ new Builder().setUsage(USAGE_VOICE_COMMUNICATION).build(),
+ new Builder().setUsage(USAGE_NOTIFICATION).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_IDLE);
+
+ assertThat(suggestedContext).isEqualTo(CALL);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_ignoresInactiveConfigurations() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_ALARM).build(),
+ new Builder().setUsage(USAGE_VOICE_COMMUNICATION).setInactive().build(),
+ new Builder().setUsage(USAGE_NOTIFICATION).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_IDLE);
+
+ assertThat(suggestedContext).isEqualTo(ALARM);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withLowerPriorityConfigurationsAndCall_returnsCall() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_ALARM).build(),
+ new Builder().setUsage(USAGE_NOTIFICATION).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_OFFHOOK);
+
+ assertThat(suggestedContext).isEqualTo(CALL);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withNavigationConfigurationAndCall_returnsNavigation() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_OFFHOOK);
+
+ assertThat(suggestedContext).isEqualTo(NAVIGATION);
+ }
+
+ @Test
+ public void getSuggestedAudioContext_withUnprioritizedUsage_returnsDefault() {
+ List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+ new Builder().setUsage(USAGE_VIRTUAL_SOURCE).build()
+ );
+
+ @AudioContext int suggestedContext = CarVolume.getSuggestedAudioContext(configurations,
+ CALL_STATE_IDLE);
+
+ assertThat(suggestedContext).isEqualTo(DEFAULT_AUDIO_CONTEXT);
+ }
+
+ private static class Builder {
+ private @AttributeUsage int mUsage = USAGE_MEDIA;
+ private boolean mIsActive = true;
+
+ Builder setUsage(@AttributeUsage int usage) {
+ mUsage = usage;
+ return this;
+ }
+
+ Builder setInactive() {
+ mIsActive = false;
+ return this;
+ }
+
+ AudioPlaybackConfiguration build() {
+ AudioPlaybackConfiguration configuration = mock(AudioPlaybackConfiguration.class);
+ AudioAttributes attributes = new AudioAttributes.Builder().setUsage(mUsage).build();
+ when(configuration.getAudioAttributes()).thenReturn(attributes);
+ when(configuration.isActive()).thenReturn(mIsActive);
+ return configuration;
+ }
+ }
+}