Merge "Updating getSuggestedAudioUsage" into rvc-dev
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;
+        }
+    }
+}