Add CarVolumeService

Things added in this cl:
+ Hook up hardware volume keys to CarVolumeService to directly update
  volume for current audio context.
+ Added volume control apis in AudioManager, and the real implementation
  is done in CarVolumeService
+ The volume updates from car is broadcast to listeners through
  IVolumeController api which is already in framework. SystemUI is using
  this api to listen to volume changes (through AudioManager).
+ Added new permission for volume controls

Main TODOs left:
+ Multi stream playing at the same time. This can be done through
  adjustign software mixer gain on Android side. Utility functions to
  compute the gain is added in VolumeUtils, but it's not used yet in
  CarVolumeService.

+ Hook up with Settings so per stream volume can be persisted across
  multiple boots.

Bug: 27595951

Change-Id: I3a63e423d4e0a347215af65e79926212e4503d1b
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index a24febf..56705c7 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -14,6 +14,7 @@
     field public static final int CONNECTION_TYPE_EMBEDDED = 5; // 0x5
     field public static final java.lang.String INFO_SERVICE = "info";
     field public static final java.lang.String PACKAGE_SERVICE = "package";
+    field public static final java.lang.String PERMISSION_CAR_CONTROL_AUDIO_VOLUME = "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
     field public static final java.lang.String PERMISSION_FUEL = "android.car.permission.CAR_FUEL";
     field public static final java.lang.String PERMISSION_MILEAGE = "android.car.permission.CAR_MILEAGE";
     field public static final java.lang.String PERMISSION_SPEED = "android.car.permission.CAR_SPEED";
@@ -329,8 +330,12 @@
   public class CarAudioManager {
     method public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method public android.media.AudioAttributes getAudioAttributesForCarUsage(int);
+    method public int getStreamMaxVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamMinVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamVolume(int) throws android.car.CarNotConnectedException;
     method public void onCarDisconnected();
     method public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException;
+    method public void setStreamVolume(int, int, int) throws android.car.CarNotConnectedException;
     field public static final int CAR_AUDIO_USAGE_ALARM = 6; // 0x6
     field public static final int CAR_AUDIO_USAGE_DEFAULT = 0; // 0x0
     field public static final int CAR_AUDIO_USAGE_MUSIC = 1; // 0x1
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 6a254dd..515f5ba 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -17,6 +17,7 @@
     field public static final java.lang.String INFO_SERVICE = "info";
     field public static final java.lang.String PACKAGE_SERVICE = "package";
     field public static final java.lang.String PERMISSION_CAR_CAMERA = "android.car.permission.CAR_CAMERA";
+    field public static final java.lang.String PERMISSION_CAR_CONTROL_AUDIO_VOLUME = "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
     field public static final java.lang.String PERMISSION_CAR_HVAC = "android.car.permission.CAR_HVAC";
     field public static final java.lang.String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
     field public static final java.lang.String PERMISSION_CAR_RADIO = "android.car.permission.CAR_RADIO";
@@ -688,8 +689,13 @@
   public class CarAudioManager {
     method public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method public android.media.AudioAttributes getAudioAttributesForCarUsage(int);
+    method public int getStreamMaxVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamMinVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamVolume(int) throws android.car.CarNotConnectedException;
     method public void onCarDisconnected();
     method public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException;
+    method public void setStreamVolume(int, int, int) throws android.car.CarNotConnectedException;
+    method public void setVolumeController(android.media.IVolumeController) throws android.car.CarNotConnectedException;
     field public static final int CAR_AUDIO_USAGE_ALARM = 6; // 0x6
     field public static final int CAR_AUDIO_USAGE_DEFAULT = 0; // 0x0
     field public static final int CAR_AUDIO_USAGE_MUSIC = 1; // 0x1
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index e0d91f4..b6f7cc8 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -113,6 +113,12 @@
     public static final String PERMISSION_SPEED = "android.car.permission.CAR_SPEED";
 
     /**
+     * Permission necessary to change car audio volume through {@link CarAudioManager}.
+     */
+    public static final String PERMISSION_CAR_CONTROL_AUDIO_VOLUME =
+            "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
+
+    /**
      * Permission necessary to use {@link CarNavigationManager}.
      * @hide
      */
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index 0a92c04..be4bfba 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -16,13 +16,18 @@
 package android.car.media;
 
 import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.car.CarLibLog;
+import android.car.CarNotConnectedException;
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.IVolumeController;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.car.CarManagerBase;
+import android.util.Log;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -127,6 +132,103 @@
         return mAudioManager.abandonAudioFocus(l, aa);
     }
 
+    /**
+     * Sets the volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param streamType The stream whose volume index should be set.
+     * @param index The volume index to set. See
+     *            {@link #getStreamMaxVolume(int)} for the largest valid value.
+     * @param flags One or more flags (e.g., {@link android.media.AudioManager#FLAG_SHOW_UI},
+     *              {@link android.media.AudioManager#FLAG_PLAY_SOUND})
+     */
+    @SystemApi
+    public void setStreamVolume(int streamType, int index, int flags)
+            throws CarNotConnectedException {
+        try {
+            mService.setStreamVolume(streamType, index, flags);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "setStreamVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Registers a global volume controller interface.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void setVolumeController(IVolumeController controller)
+            throws CarNotConnectedException {
+        try {
+            mService.setVolumeController(controller);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "setVolumeController failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the maximum volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose maximum volume index is returned.
+     * @return The maximum valid volume index for the stream.
+     */
+    @SystemApi
+    public int getStreamMaxVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamMaxVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamMaxVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the minimum volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose maximum volume index is returned.
+     * @return The maximum valid volume index for the stream.
+     */
+    @SystemApi
+    public int getStreamMinVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamMinVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamMaxVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the current volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose volume index is returned.
+     * @return The current volume index for the stream.
+     *
+     * @see #getStreamMaxVolume(int)
+     * @see #setStreamVolume(int, int, int)
+     */
+    @SystemApi
+    public int getStreamVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
     @Override
     public void onCarDisconnected() {
         // TODO Auto-generated method stub
diff --git a/car-lib/src/android/car/media/ICarAudio.aidl b/car-lib/src/android/car/media/ICarAudio.aidl
index 7a5e43d..dfa2573 100644
--- a/car-lib/src/android/car/media/ICarAudio.aidl
+++ b/car-lib/src/android/car/media/ICarAudio.aidl
@@ -17,6 +17,7 @@
 package android.car.media;
 
 import android.media.AudioAttributes;
+import android.media.IVolumeController;
 
 /**
  * Binder interface for {@link android.car.media.CarAudioManager}.
@@ -26,4 +27,9 @@
  */
 interface ICarAudio {
     AudioAttributes getAudioAttributesForCarUsage(int carUsage) = 0;
+    void setStreamVolume(int streamType, int index, int flags) = 1;
+    void setVolumeController(IVolumeController controller) = 2;
+    int getStreamMaxVolume(int streamType) = 3;
+    int getStreamMinVolume(int streamType) = 4;
+    int getStreamVolume(int streamType) = 5;
 }
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 73dd518..4df72e4 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -88,6 +88,12 @@
         android:label="@string/car_permission_label_control_app_blocking"
         android:description="@string/car_permission_desc_control_app_blocking" />
 
+    <permission
+        android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"
+        android:protectionLevel="system|signature"
+        android:label="@string/car_permission_label_audio_volume"
+        android:description="@string/car_permission_desc_audio_volume" />
+
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.DEVICE_POWER" />
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index d9dc3d0..3721fb3 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -50,12 +50,16 @@
     <string name="car_permission_desc_radio">Access your car\'s radio.</string>
     <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_projection">Car Projection</string>
+    <!-- Permission text: apps can control car-audio-volume [CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_audio_volume">Car Audio Volume</string>
     <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_projection">Project phone interface on car display.</string>
     <string name="car_permission_label_mock_vehicle_hal">Emulate vehicle HAL</string>
     <!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_mock_vehicle_hal">Emulate your car\'s vehicle HAL for internal
         testing purpose.</string>
+    <!-- Permission text: can adjust the audio volume on your car [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_audio_volume">Control your car\'s audio volume.</string>
     <string name="car_permission_label_control_app_blocking">Application blocking</string>
     <!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_control_app_blocking">Control application blocking while
diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java
index 8bba78d..3c90bde 100644
--- a/service/src/com/android/car/CarAudioService.java
+++ b/service/src/com/android/car/CarAudioService.java
@@ -15,10 +15,13 @@
  */
 package com.android.car;
 
+import android.car.Car;
 import android.car.VehicleZoneUtil;
+import android.app.AppGlobals;
 import android.car.media.CarAudioManager;
 import android.car.media.ICarAudio;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceInfo;
@@ -27,12 +30,15 @@
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioMix;
 import android.media.audiopolicy.AudioMixingRule;
+import android.media.IVolumeController;
 import android.media.audiopolicy.AudioPolicy;
 import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.car.hal.AudioHalService;
@@ -69,7 +75,7 @@
     private final HandlerThread mFocusHandlerThread;
     private final CarAudioFocusChangeHandler mFocusHandler;
     private final SystemFocusListener mSystemFocusListener;
-
+    private final CarVolumeService mVolumeService;
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private AudioPolicy mAudioPolicy;
@@ -125,7 +131,7 @@
             CarAudioAttributesUtil.getAudioAttributesForCarUsage(
                     CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY);
 
-    public CarAudioService(Context context) {
+    public CarAudioService(Context context, CarInputService inputService) {
         mAudioHal = VehicleHal.getInstance().getAudioHal();
         mContext = context;
         mFocusHandlerThread = new HandlerThread(CarLog.TAG_AUDIO);
@@ -139,6 +145,7 @@
         mNumConsecutiveHalFailuresForCanError =
                 (int) res.getInteger(R.integer.consecutiveHalFailures);
         mUseDynamicRouting = res.getBoolean(R.bool.audioUseDynamicRouting);
+        mVolumeService = new CarVolumeService(mContext, this, mAudioHal, inputService);
     }
 
     @Override
@@ -188,6 +195,7 @@
             mAudioRoutingPolicy = audioRoutingPolicy;
             mIsRadioExternal = mAudioHal.isRadioExternal();
         }
+        mVolumeService.init();
     }
 
     private void setupDynamicRoutng(AudioRoutingPolicy audioRoutingPolicy,
@@ -367,6 +375,48 @@
         mFocusHandler.handleStreamStateChange(state, streamNumber);
     }
 
+    @Override
+    public void setStreamVolume(int streamType, int index, int flags) {
+        enforceAudioVolumePermission();
+        mVolumeService.setStreamVolume(streamType, index, flags);
+    }
+
+    @Override
+    public void setVolumeController(IVolumeController controller) {
+        enforceAudioVolumePermission();
+        mVolumeService.setVolumeController(controller);
+    }
+
+    @Override
+    public int getStreamMaxVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamMaxVolume(streamType);
+    }
+
+    @Override
+    public int getStreamMinVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamMinVolume(streamType);
+    }
+
+    @Override
+    public int getStreamVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamVolume(streamType);
+    }
+
+    public AudioRoutingPolicy getAudioRoutingPolicy() {
+        return mAudioRoutingPolicy;
+    }
+
+    private void enforceAudioVolumePermission() {
+        if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "requires permission " + Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+        }
+    }
+
     private void doHandleCarFocusChange() {
         int newFocusState = AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_INVALID;
         FocusState currentState;
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 6340549..27e72dc 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -35,7 +35,7 @@
 public class CarInputService implements CarServiceBase, InputHalService.InputListener {
 
     public interface KeyEventListener {
-        void onKeyEvent(KeyEvent event);
+        boolean onKeyEvent(KeyEvent event);
     }
 
     private static final long VOICE_LONG_PRESS_TIME_MS = 1000;
@@ -210,14 +210,13 @@
     }
 
     private void handleVolumeKey(KeyEvent event) {
-        KeyEventListener listener = null;
+        KeyEventListener listener;
         synchronized (this) {
             listener = mVolumeKeyListener;
         }
-        if (listener == null) {
-            return;
+        if (listener != null) {
+            listener.onKeyEvent(event);
         }
-        listener.onKeyEvent(event);
     }
 
     private void handleMainDisplayKey(KeyEvent event) {
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 35e6494..1c5f15d 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -44,16 +44,18 @@
     private final CarInputService.KeyEventListener mVoiceAssistantKeyListener =
             new CarInputService.KeyEventListener() {
                 @Override
-                public void onKeyEvent(KeyEvent event) {
+                public boolean onKeyEvent(KeyEvent event) {
                     handleVoiceAssitantRequest(false);
+                    return true;
                 }
             };
 
     private final CarInputService.KeyEventListener mLongVoiceAssistantKeyListener =
             new CarInputService.KeyEventListener() {
                 @Override
-                public void onKeyEvent(KeyEvent event) {
+                public boolean onKeyEvent(KeyEvent event) {
                     handleVoiceAssitantRequest(true);
+                    return true;
                 }
             };
 
diff --git a/service/src/com/android/car/CarVolumeControllerFactory.java b/service/src/com/android/car/CarVolumeControllerFactory.java
new file mode 100644
index 0000000..72cf310
--- /dev/null
+++ b/service/src/com/android/car/CarVolumeControllerFactory.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.IVolumeController;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+
+import com.android.car.CarVolumeService.CarVolumeController;
+import com.android.car.hal.AudioHalService;
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based
+ * on car properties.
+ */
+public class CarVolumeControllerFactory {
+
+    public static CarVolumeController createCarVolumeController(Context context,
+            CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) {
+        final boolean volumeSupported = audioHal.isAudioVolumeSupported();
+
+        // Case 1: Car Audio Module does not support volume controls
+        if (!volumeSupported) {
+            return new SimpleCarVolumeController(context);
+        }
+        return new CarExternalVolumeController(context, audioService, audioHal, inputService);
+    }
+
+    /**
+     * To control volumes through {@link android.media.AudioManager} when car audio module does not
+     * support volume controls.
+     */
+    public static final class SimpleCarVolumeController extends CarVolumeController {
+        private final AudioManager mAudioManager;
+        private final Context mContext;
+
+        public SimpleCarVolumeController(Context context) {
+            mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+            mContext = context;
+        }
+
+        @Override
+        void init() {
+        }
+
+        @Override
+        public void setStreamVolume(int stream, int index, int flags) {
+            mAudioManager.setStreamVolume(stream, index, flags);
+        }
+
+        @Override
+        public int getStreamVolume(int stream) {
+            return mAudioManager.getStreamVolume(stream);
+        }
+
+        @Override
+        public void setVolumeController(IVolumeController controller) {
+            mAudioManager.setVolumeController(controller);
+        }
+
+        @Override
+        public int getStreamMaxVolume(int stream) {
+            return mAudioManager.getStreamMaxVolume(stream);
+        }
+
+        @Override
+        public int getStreamMinVolume(int stream) {
+            return mAudioManager.getStreamMinVolume(stream);
+        }
+
+        @Override
+        public boolean onKeyEvent(KeyEvent event) {
+            handleVolumeKeyDefault(event);
+            return true;
+        }
+
+        private void handleVolumeKeyDefault(KeyEvent event) {
+            if (event.getAction() != KeyEvent.ACTION_DOWN) {
+                return;
+            }
+
+            boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP;
+            int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND
+                    | AudioManager.FLAG_FROM_KEY;
+            IAudioService audioService = getAudioService();
+            String pkgName = mContext.getOpPackageName();
+            try {
+                if (audioService != null) {
+                    audioService.adjustSuggestedStreamVolume(
+                            volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER,
+                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT);
+                }
+            } catch (RemoteException e) {
+                Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e);
+            }
+        }
+
+        private static IAudioService getAudioService() {
+            IAudioService audioService = IAudioService.Stub.asInterface(
+                    ServiceManager.checkService(Context.AUDIO_SERVICE));
+            if (audioService == null) {
+                Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface.");
+            }
+            return audioService;
+        }
+    }
+
+    /**
+     * The car volume controller to use when the car audio modules supports volume controls.
+     *
+     * Depending on whether the car support audio context and has persistent memory, we need to
+     * handle per context volume change properly.
+     *
+     * Regardless whether car supports audio context or not, we need to keep per audio context
+     * volume internally. If we only support single channel, then we only send the volume change
+     * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on
+     * software mixer level or send it the car audio module if the car support audio context
+     * and multi channel. TODO: Add support for multi channel.
+     *
+     * Per context volume should be persisted, so the volumes can stay the same across boots.
+     * Depending on the hardware property, this can be persisted on car side (or/and android side).
+     * TODO: we need to define one single source of truth if the car has memory.
+     */
+    public static class CarExternalVolumeController extends CarVolumeController
+            implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener,
+            CarAudioService.AudioContextChangeListener {
+        private static final String TAG = CarLog.TAG_AUDIO + "ExtVolCtrl";
+        private static final int MSG_UPDATE_VOLUME = 0;
+        private static final int MSG_UPDATE_HAL = 1;
+
+        private final Context mContext;
+        private final AudioRoutingPolicy mPolicy;
+        private final AudioHalService mHal;
+        private final CarInputService mInputService;
+        private final CarAudioService mAudioService;
+
+        private int mSupportedAudioContext;
+
+        private boolean mHasExternalMemory;
+
+        @GuardedBy("this")
+        private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
+        // current logical volume, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mCurrentLogicalVolume =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+        // stream volume limit, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mLogicalStreamVolumeMax =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+        // stream volume limit, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mLogicalStreamVolumeMin =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+
+        @GuardedBy("this")
+        private final RemoteCallbackList<IVolumeController> mVolumeControllers =
+                new RemoteCallbackList<>();
+
+        private final Handler mHandler = new VolumeHandler();
+
+        /**
+         * Convert an android logical stream to the car stream.
+         *
+         * @return If car supports audio context, then it returns the car audio context. Otherwise,
+         *      it returns the physical stream that maps to this logical stream.
+         */
+        private int logicalStreamToCarStream(int logicalAndroidStream) {
+            if (mSupportedAudioContext == 0) {
+                int physicalStream = mPolicy.getPhysicalStreamForLogicalStream(
+                        CarVolumeService.androidStreamToCarUsage(logicalAndroidStream));
+                return physicalStream;
+            } else {
+                int carContext = VolumeUtils.androidStreamToCarContext(logicalAndroidStream);
+                if ((carContext & mSupportedAudioContext) == 0) {
+                    carContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
+                }
+                return carContext;
+            }
+        }
+
+        /**
+         * All updates to external components should be posted to this handler to avoid holding
+         * the internal lock while sending updates.
+         */
+        private final class VolumeHandler extends Handler {
+            @Override
+            public void handleMessage(Message msg) {
+                int stream;
+                int volume;
+                switch (msg.what) {
+                    case MSG_UPDATE_VOLUME:
+                        stream = msg.arg1;
+                        int flag = msg.arg2;
+                        final int size = mVolumeControllers.beginBroadcast();
+                        try {
+                            for (int i = 0; i < size; i++) {
+                                try {
+                                    mVolumeControllers.getBroadcastItem(i)
+                                            .volumeChanged(stream, flag);
+                                } catch (RemoteException ignored) {
+                                }
+                            }
+                        } finally {
+                            mVolumeControllers.finishBroadcast();
+                        }
+                        break;
+                    case MSG_UPDATE_HAL:
+                        stream = msg.arg1;
+                        volume = msg.arg2;
+                        mHal.setStreamVolume(stream, volume);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        public CarExternalVolumeController(Context context, CarAudioService audioService,
+                                           AudioHalService hal, CarInputService inputService) {
+            mContext = context;
+            mAudioService = audioService;
+            mPolicy = audioService.getAudioRoutingPolicy();
+            mHal = hal;
+            mInputService = inputService;
+        }
+
+        @Override
+        void init() {
+            mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts();
+            mHasExternalMemory = mHal.isExternalAudioVolumePersistent();
+            synchronized (this) {
+                initVolumeLimitLocked();
+                initCurrentVolumeLocked();
+            }
+            mInputService.setVolumeKeyListener(this);
+            mHal.setVolumeListener(this);
+            mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this);
+        }
+
+        private void initVolumeLimitLocked() {
+            for (int i : VolumeUtils.LOGICAL_STREAMS) {
+                int carStream = logicalStreamToCarStream(i);
+                Pair<Integer, Integer> volumeMinMax = mHal.getStreamVolumeLimit(carStream);
+                int max;
+                int min;
+                if (volumeMinMax == null) {
+                    max = 0;
+                    min = 0;
+                } else {
+                    max = volumeMinMax.second >= 0 ? volumeMinMax.second : 0;
+                    min = volumeMinMax.first >=0 ? volumeMinMax.first : 0;
+                }
+                // get default stream volume limit first.
+                mLogicalStreamVolumeMax.put(i, max);
+                mLogicalStreamVolumeMin.put(i, min);
+            }
+        }
+
+        private void initCurrentVolumeLocked() {
+            if (mHasExternalMemory) {
+                // TODO: read per context volume from audio hal
+            } else {
+                // TODO: read the Android side volume from Settings and pass it to the audio module
+                // Here we just set it to the physical stream volume temporarily.
+                for (int i : VolumeUtils.LOGICAL_STREAMS) {
+                    mCurrentLogicalVolume.put(i, mHal.getStreamVolume(logicalStreamToCarStream(i)));
+                }
+            }
+        }
+
+        @Override
+        public void setStreamVolume(int stream, int index, int flags) {
+            synchronized (this) {
+                setStreamVolumeInternalLocked(stream, index, flags);
+            }
+        }
+
+        private void setStreamVolumeInternalLocked(int stream, int index, int flags) {
+            if (mLogicalStreamVolumeMax.get(stream) == null) {
+                Log.e(TAG, "Stream type not supported " + stream);
+                return;
+            }
+            int limit = mLogicalStreamVolumeMax.get(stream);
+            if (index > limit) {
+                Log.e(TAG, "Volume exceeds volume limit. stream: " + stream + " index: " + index
+                        + " limit: " + limit);
+                index = limit;
+            }
+
+            if (index < 0) {
+                index = 0;
+            }
+
+            if (mCurrentLogicalVolume.get(stream) == index) {
+                return;
+            }
+
+            int carStream = logicalStreamToCarStream(stream);
+            int carContext = VolumeUtils.androidStreamToCarContext(stream);
+
+            // For single channel, only adjust the volume when the audio context is the current one.
+            if (mCurrentContext == carContext) {
+                mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
+            }
+            // Record the current volume internally.
+            mCurrentLogicalVolume.put(stream, index);
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_VOLUME, stream,
+                    getVolumeUpdateFlag()));
+        }
+
+        @Override
+        public int getStreamVolume(int stream) {
+            synchronized (this) {
+                if (mCurrentLogicalVolume.get(stream) == null) {
+                    Log.d(TAG, "Invalid stream type " + stream);
+                    return 0;
+                }
+                return mCurrentLogicalVolume.get(stream);
+            }
+        }
+
+        @Override
+        public void setVolumeController(IVolumeController controller) {
+            synchronized (this) {
+                mVolumeControllers.register(controller);
+            }
+        }
+
+        @Override
+        public void onVolumeChange(int carStream, int volume, int volumeState) {
+            int flag = getVolumeUpdateFlag();
+            synchronized (this) {
+                // Assume single channel here.
+                int currentLogicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
+                int currentCarStream = logicalStreamToCarStream(currentLogicalStream);
+                if (currentCarStream == carStream) {
+                    mCurrentLogicalVolume.put(currentLogicalStream, volume);
+                    mHandler.sendMessage(
+                            mHandler.obtainMessage(MSG_UPDATE_VOLUME, currentLogicalStream, flag));
+                } else {
+                    // Hal is telling us a car stream volume has changed, but it is not the current
+                    // stream.
+                    Log.w(TAG, "Car stream" + carStream
+                            + " volume changed, but it is not current stream, ignored.");
+                }
+            }
+        }
+
+        private int getVolumeUpdateFlag() {
+            // TODO: Apply appropriate flags.
+            return AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND;
+        }
+
+        private void updateHalVolumeLocked(final int carStream, final int index) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
+        }
+
+        @Override
+        public void onVolumeLimitChange(int streamNumber, int volume) {
+            // TODO: How should this update be sent to SystemUI? maybe send a volume update without
+            // showing UI.
+            synchronized (this) {
+                initVolumeLimitLocked();
+            }
+        }
+
+        @Override
+        public int getStreamMaxVolume(int stream) {
+            synchronized (this) {
+                if (mLogicalStreamVolumeMax.get(stream) == null) {
+                    Log.e(TAG, "Stream type not supported " + stream);
+                    return 0;
+                }
+                return mLogicalStreamVolumeMax.get(stream);
+            }
+        }
+
+        @Override
+        public int getStreamMinVolume(int stream) {
+            synchronized (this) {
+                if (mLogicalStreamVolumeMin.get(stream) == null) {
+                    Log.e(TAG, "Stream type not supported " + stream);
+                    return 0;
+                }
+                return mLogicalStreamVolumeMin.get(stream);
+            }
+        }
+
+        @Override
+        public boolean onKeyEvent(KeyEvent event) {
+            int logicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
+            final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+            // TODO: properly handle long press on volume key
+            if (!down) {
+                return true;
+            }
+
+            synchronized (this) {
+                int currentVolume = mCurrentLogicalVolume.get(logicalStream);
+                switch (event.getKeyCode()) {
+                    case KeyEvent.KEYCODE_VOLUME_UP:
+                        setStreamVolumeInternalLocked(logicalStream, currentVolume + 1,
+                                getVolumeUpdateFlag());
+                        break;
+                    case KeyEvent.KEYCODE_VOLUME_DOWN:
+                        setStreamVolumeInternalLocked(logicalStream, currentVolume - 1,
+                                getVolumeUpdateFlag());
+                        break;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) {
+            synchronized (this) {
+                if (primaryFocusContext == mCurrentContext) {
+                    return;
+                }
+                mCurrentContext = primaryFocusContext;
+
+                int currentVolume = mCurrentLogicalVolume.get(
+                        VolumeUtils.carContextToAndroidStream(primaryFocusContext));
+                if (mSupportedAudioContext == 0) {
+                    // Car does not support audio context, we need to reset the volume
+                    updateHalVolumeLocked(primaryFocusPhysicalStream, currentVolume);
+                } else {
+                    // car supports context, but does not have memory.
+                    if (!mHasExternalMemory) {
+                        updateHalVolumeLocked(primaryFocusContext, currentVolume);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/car/CarVolumeService.java b/service/src/com/android/car/CarVolumeService.java
new file mode 100644
index 0000000..18ab597
--- /dev/null
+++ b/service/src/com/android/car/CarVolumeService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.IVolumeController;
+import android.view.KeyEvent;
+
+import com.android.car.hal.AudioHalService;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioContextFlag;
+
+/**
+ * Handles car volume controls.
+ *
+ * It delegates to a {@link com.android.car.CarVolumeService.CarVolumeController} to do proper
+ * volume controls based on different car properties.
+ *
+ * @hide
+ */
+public class CarVolumeService {
+    private static final String TAG = "CarVolumeService";
+
+    // TODO: need to have a policy to define the default context
+    public static int DEFAULT_CAR_AUDIO_CONTEXT =
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG;
+    private final Context mContext;
+    private final AudioHalService mAudioHal;
+    private final CarAudioService mAudioService;
+    private final CarInputService mInputService;
+
+    private CarVolumeController mCarVolumeController;
+
+    public static int androidStreamToCarUsage(int logicalAndroidStream) {
+        return CarAudioAttributesUtil.getCarUsageFromAudioAttributes(
+                new AudioAttributes.Builder()
+                        .setLegacyStreamType(logicalAndroidStream).build());
+    }
+
+    public CarVolumeService(Context context, CarAudioService audioService, AudioHalService audioHal,
+                            CarInputService inputService) {
+        mContext = context;
+        mAudioHal = audioHal;
+        mAudioService = audioService;
+        mInputService = inputService;
+    }
+
+    public void init() {
+        mCarVolumeController = CarVolumeControllerFactory.createCarVolumeController(mContext,
+                mAudioService, mAudioHal, mInputService);
+
+        mCarVolumeController.init();
+        mInputService.setVolumeKeyListener(mCarVolumeController);
+    }
+
+    public void setStreamVolume(int streamType, int index, int flags) {
+        mCarVolumeController.setStreamVolume(streamType, index, flags);
+    }
+
+    public void setVolumeController(IVolumeController controller) {
+        mCarVolumeController.setVolumeController(controller);
+    }
+
+    public int getStreamMaxVolume(int stream) {
+        return mCarVolumeController.getStreamMaxVolume(stream);
+    }
+
+    public int getStreamMinVolume(int stream) {
+        return mCarVolumeController.getStreamMinVolume(stream);
+    }
+
+    public int getStreamVolume(int stream) {
+        return mCarVolumeController.getStreamVolume(stream);
+    }
+
+    /**
+     * Abstraction layer for volume controls, so that we don't have if-else check for audio
+     * properties everywhere.
+     */
+    public static abstract class CarVolumeController implements CarInputService.KeyEventListener {
+        abstract void init();
+        abstract public void setStreamVolume(int stream, int index, int flags);
+        abstract public void setVolumeController(IVolumeController controller);
+        abstract public int getStreamMaxVolume(int stream);
+        abstract public int getStreamMinVolume(int stream);
+        abstract public int getStreamVolume(int stream);
+    }
+}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index a1e55be..87f827c 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -94,7 +94,7 @@
         mCarInfoService = new CarInfoService(serviceContext);
         mAppContextService = new AppContextService(serviceContext);
         mCarSensorService = new CarSensorService(serviceContext);
-        mCarAudioService = new CarAudioService(serviceContext);
+        mCarAudioService = new CarAudioService(serviceContext, mCarInputService);
         mCarHvacService = new CarHvacService(serviceContext);
         mCarRadioService = new CarRadioService(serviceContext);
         mCarCameraService = new CarCameraService(serviceContext);
diff --git a/service/src/com/android/car/VolumeUtils.java b/service/src/com/android/car/VolumeUtils.java
new file mode 100644
index 0000000..023d319
--- /dev/null
+++ b/service/src/com/android/car/VolumeUtils.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioContextFlag;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class VolumeUtils {
+    private static final String TAG = "VolumeUtils";
+
+    public static final int[] LOGICAL_STREAMS = {
+            AudioManager.STREAM_VOICE_CALL,
+            AudioManager.STREAM_SYSTEM,
+            AudioManager.STREAM_RING,
+            AudioManager.STREAM_MUSIC,
+            AudioManager.STREAM_ALARM,
+            AudioManager.STREAM_NOTIFICATION,
+            AudioManager.STREAM_DTMF,
+    };
+
+    public static final int[] CAR_AUDIO_CONTEXT = {
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_VOICE_COMMAND_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SAFETY_ALERT_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CD_ROM_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_AUX_AUDIO_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG
+    };
+
+    public static String streamToName(int stream) {
+        switch (stream) {
+            case AudioManager.STREAM_ALARM: return "Alarm";
+            case AudioManager.STREAM_MUSIC: return "Music";
+            case AudioManager.STREAM_NOTIFICATION: return "Notification";
+            case AudioManager.STREAM_RING: return "Ring";
+            case AudioManager.STREAM_VOICE_CALL: return "Call";
+            case AudioManager.STREAM_SYSTEM: return "System";
+            case AudioManager.STREAM_DTMF: return "DTMF";
+            default: return "Unknown";
+        }
+    }
+
+    public static int androidStreamToCarContext(int logicalAndroidStream) {
+        switch (logicalAndroidStream) {
+            case AudioManager.STREAM_VOICE_CALL:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG;
+            case AudioManager.STREAM_SYSTEM:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
+            case AudioManager.STREAM_RING:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG;
+            case AudioManager.STREAM_MUSIC:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG;
+            case AudioManager.STREAM_ALARM:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG;
+            case AudioManager.STREAM_NOTIFICATION:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG;
+            case AudioManager.STREAM_DTMF:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
+            default:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG;
+        }
+    }
+
+    public static int carContextToAndroidStream(int carContext) {
+        switch (carContext) {
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG:
+                return AudioManager.STREAM_VOICE_CALL;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG:
+                return AudioManager.STREAM_SYSTEM;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG:
+                return AudioManager.STREAM_NOTIFICATION;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG:
+                return AudioManager.STREAM_MUSIC;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG:
+                return AudioManager.STREAM_ALARM;
+            default:
+                return AudioManager.STREAM_MUSIC;
+        }
+    }
+
+    public static int androidStreamToCarUsage(int logicalAndroidStream) {
+        return CarAudioAttributesUtil.getCarUsageFromAudioAttributes(
+                new AudioAttributes.Builder()
+                        .setLegacyStreamType(logicalAndroidStream).build());
+    }
+
+    private final SparseArray<Float[]> mStreamAmplLookup = new SparseArray<>(7);
+
+    private static final float LN_10 = 2.302585093f;
+    // From cs/#android/frameworks/av/media/libmedia/AudioSystem.cpp
+    private static final float DB_PER_STEP = -.5f;
+
+    private final AudioManager mAudioManager;
+
+    public VolumeUtils(AudioManager audioManager) {
+        mAudioManager = audioManager;
+        for(int i : LOGICAL_STREAMS) {
+            initStreamLookup(i);
+        }
+    }
+
+    private void initStreamLookup(int streamType) {
+        int maxIndex = mAudioManager.getStreamMaxVolume(streamType);
+        Float[] amplList = new Float[maxIndex + 1];
+
+        for (int i = 0; i <= maxIndex; i++) {
+            amplList[i] = volIndexToAmpl(i, maxIndex);
+        }
+        Log.d(TAG, streamToName(streamType) + ": " + Arrays.toString(amplList));
+        mStreamAmplLookup.put(streamType, amplList);
+    }
+
+
+    public static int closestIndex(float desired, Float[] list) {
+        float min = Float.MAX_VALUE;
+        int closestIndex = 0;
+
+        for (int i = 0; i < list.length; i++) {
+            float diff = Math.abs(list[i] - desired);
+            if (diff < min) {
+                min = diff;
+                closestIndex = i;
+            }
+        }
+        return closestIndex;
+    }
+
+    public void adjustStreamVol(int stream, int desired, int actual, int maxIndex) {
+        float gain = getTrackGain(desired, actual, maxIndex);
+        int index = closestIndex(gain, mStreamAmplLookup.get(stream));
+        if (index == mAudioManager.getStreamVolume(stream)) {
+            return;
+        } else {
+            mAudioManager.setStreamVolume(stream, index, 0 /*don't show UI*/);
+        }
+    }
+
+    /**
+     * Returns the gain which, when applied to an a stream with volume
+     * actualVolIndex, will make the output volume equivalent to a stream with a gain of
+     * 1.0 playing on a stream with volume desiredVolIndex.
+     *
+     * Computing this is non-trivial because the gain is applied on a linear scale while the volume
+     * indices map to a log (dB) scale.
+     *
+     * The computation is copied from cs/#android/frameworks/av/media/libmedia/AudioSystem.cpp
+     */
+    float getTrackGain(int desiredVolIndex, int actualVolIndex, int maxIndex) {
+        if (desiredVolIndex == actualVolIndex) {
+            return 1.0f;
+        }
+        return volIndexToAmpl(desiredVolIndex, maxIndex)
+                / volIndexToAmpl(actualVolIndex, maxIndex);
+    }
+
+    /**
+     * Returns the amplitude corresponding to volIndex. Guaranteed to return a non-negative value.
+     */
+    private float volIndexToAmpl(int volIndex, int maxIndex) {
+        // Normalize volIndex to be in the range [0, 100].
+        int volume = (int) ((float) volIndex / maxIndex * 100.0f);
+        return logToLinear(volumeToDecibels(volume));
+    }
+
+    /**
+     * volume is in the range [0, 100].
+     */
+    private static float volumeToDecibels(int volume) {
+        return (100 - volume) * DB_PER_STEP;
+    }
+
+    /**
+     * Corresponds to the function linearToLog in AudioSystem.cpp.
+     */
+    private static float logToLinear(float decibels) {
+        return decibels < 0.0f ? (float) Math.exp(decibels * LN_10 / 20.0f) : 1.0f;
+    }
+}
\ No newline at end of file
diff --git a/service/src/com/android/car/hal/AudioHalService.java b/service/src/com/android/car/hal/AudioHalService.java
index d9906cd..0e97e4b 100644
--- a/service/src/com/android/car/hal/AudioHalService.java
+++ b/service/src/com/android/car/hal/AudioHalService.java
@@ -15,9 +15,11 @@
  */
 package com.android.car.hal;
 
+import android.car.VehicleZoneUtil;
 import android.car.media.CarAudioManager;
 import android.os.ServiceSpecificException;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.car.AudioRoutingPolicy;
 import com.android.car.CarAudioAttributesUtil;
@@ -39,6 +41,7 @@
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfigs;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
+import com.android.car.vehiclenetwork.VehiclePropValueUtil;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -46,7 +49,6 @@
 import java.util.List;
 
 public class AudioHalService extends HalServiceBase {
-
     public static final int VEHICLE_AUDIO_FOCUS_REQUEST_INVALID = -1;
     public static final int VEHICLE_AUDIO_FOCUS_REQUEST_GAIN =
             VehicleAudioFocusRequest.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN;
@@ -205,6 +207,44 @@
     }
 
     /**
+     * Returns the volume limits of a stream in the form <min, max>.
+     */
+    public Pair<Integer, Integer> getStreamVolumeLimit(int stream) {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        int supportedContext = getSupportedAudioVolumeContexts();
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        List<Integer> maxs = config.getInt32MaxsList();
+        List<Integer> mins = config.getInt32MinsList();
+
+        if (maxs.size() != mins.size()) {
+            Log.e(CarLog.TAG_AUDIO, "Invalid volume prop config");
+            return null;
+        }
+
+        Pair<Integer, Integer> result = null;
+        if (supportedContext != 0) {
+            int index = VehicleZoneUtil.zoneToIndex(supportedContext, stream);
+            if (index < maxs.size()) {
+                result = new Pair<>(mins.get(index), maxs.get(index));
+            }
+        } else {
+            if (stream < maxs.size()) {
+                result = new Pair<>(mins.get(stream), maxs.get(stream));
+            }
+        }
+
+        if (result == null) {
+            Log.e(CarLog.TAG_AUDIO, "No min/max volume found in vehicle" +
+                    " prop config for stream: " + stream);
+        }
+
+        return result;
+    }
+
+    /**
      * Convert car audio manager stream type (usage) into audio context type.
      */
     public static int logicalStreamToHalContextType(int logicalStream) {
@@ -249,6 +289,37 @@
                 VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, payload);
     }
 
+    public void setStreamVolume(int streamType, int index) {
+        int[] payload = {streamType, index, 0};
+        mVehicleHal.getVehicleNetwork().setIntVectorProperty(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, payload);
+    }
+
+    public int getStreamVolume(int stream) {
+        int[] volume = {stream, 0, 0};
+        VehiclePropValue streamVolume =
+                VehiclePropValueUtil.createIntVectorValue(
+                        VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, volume, 0);
+        VehiclePropValue value = mVehicleHal.getVehicleNetwork().getProperty(streamVolume);
+
+        if (value.getInt32ValuesCount() != 3) {
+            Log.e(CarLog.TAG_AUDIO, "returned value not valid");
+            throw new IllegalStateException("Invalid preset returned from service: "
+                    + value.getInt32ValuesList());
+        }
+
+        int retStreamNum = value.getInt32Values(0);
+        int retVolume = value.getInt32Values(1);
+        int retVolumeState = value.getInt32Values(2);
+
+        if (retStreamNum != stream) {
+            Log.e(CarLog.TAG_AUDIO, "Stream number is not the same: "
+                    + stream + " vs " + retStreamNum);
+            throw new IllegalStateException("Stream number is not the same");
+        }
+        return retVolume;
+    }
+
     public synchronized int getHwVariant() {
         return mVariant;
     }