resolve merge conflicts of 235f8ac to nyc-car-dev

Change-Id: I49327e591ef08875a92928535a1a0c69f7368804
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 5d56948..e0d91f4 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -155,8 +155,9 @@
     @SystemApi
     public static final String PERMISSION_CAR_RADIO = "android.car.permission.CAR_RADIO";
 
+
     /**
-     * Permission necesary to access Car PRJECTION system APIs.
+     * Permission necessary to access Car PROJECTION system APIs.
      * @hide
      */
     @SystemApi
diff --git a/car-lib/src/android/car/CarAppContextManager.java b/car-lib/src/android/car/CarAppContextManager.java
index c7657ca..22e36bf 100644
--- a/car-lib/src/android/car/CarAppContextManager.java
+++ b/car-lib/src/android/car/CarAppContextManager.java
@@ -51,7 +51,7 @@
         /**
          * Lost ownership for the context, which happens when other app has set the context.
          * The app losing context should stop the action associated with the context.
-         * For example, navigaiton app currently running active navigation should stop navigation
+         * For example, navigation app currently running active navigation should stop navigation
          * upon getting this for {@link CarAppContextManager#APP_CONTEXT_NAVIGATION}.
          * @param context
          */
@@ -89,7 +89,7 @@
         mService = IAppContext.Stub.asInterface(service);
         mHandler = new Handler(looper);
         mBinderListener = new IAppContextListenerImpl(this);
-        mOwnershipListeners = new HashMap<Integer, AppContextOwnershipChangeListener>();
+        mOwnershipListeners = new HashMap<>();
     }
 
     /**
diff --git a/car-support-lib/api/current.txt b/car-support-lib/api/current.txt
index 583fdc9..8024aaa 100644
--- a/car-support-lib/api/current.txt
+++ b/car-support-lib/api/current.txt
@@ -18,6 +18,7 @@
     field public static final int CONNECTION_TYPE_EMBEDDED = 5; // 0x5
     field public static final int CONNECTION_TYPE_EMULATOR = 0; // 0x0
     field public static final int CONNECTION_TYPE_ON_DEVICE_EMULATOR = 3; // 0x3
+    field public static final int CONNECTION_TYPE_UNKNOWN = -1; // 0xffffffff
     field public static final int CONNECTION_TYPE_USB = 1; // 0x1
     field public static final int CONNECTION_TYPE_WIFI = 2; // 0x2
     field public static final java.lang.String INFO_SERVICE = "info";
@@ -60,6 +61,14 @@
     method public abstract java.lang.Integer getInt(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
     method public abstract java.lang.Long getLong(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
     method public abstract java.lang.String getString(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
+    field public static final int DRIVER_SIDE_CENTER = 2; // 0x2
+    field public static final int DRIVER_SIDE_LEFT = 0; // 0x0
+    field public static final int DRIVER_SIDE_RIGHT = 1; // 0x1
+    field public static final java.lang.String KEY_DRIVER_POSITION = "driverPosition";
+    field public static final java.lang.String KEY_HEAD_UNIT_MAKE = "headUnitMake";
+    field public static final java.lang.String KEY_HEAD_UNIT_MODEL = "headUnitModel";
+    field public static final java.lang.String KEY_HEAD_UNIT_SOFTWARE_BUILD = "headUnitSoftwareBuild";
+    field public static final java.lang.String KEY_HEAD_UNIT_SOFTWARE_VERSION = "headUnitSoftwareVersion";
     field public static final java.lang.String KEY_MANUFACTURER = "manufacturer";
     field public static final java.lang.String KEY_MODEL = "model";
     field public static final java.lang.String KEY_MODEL_YEAR = "model-year";
diff --git a/car-support-lib/src/android/support/car/Car.java b/car-support-lib/src/android/support/car/Car.java
index e5739fd..6218288 100644
--- a/car-support-lib/src/android/support/car/Car.java
+++ b/car-support-lib/src/android/support/car/Car.java
@@ -80,6 +80,8 @@
     public static final int CONNECTION_TYPE_ADB_EMULATOR = 4;
     /** Type of car connection: platform runs directly in car. */
     public static final int CONNECTION_TYPE_EMBEDDED = 5;
+    /** Unknown type.  The support lib is likely out of date.*/
+    public static final int CONNECTION_TYPE_UNKNOWN = -1;
     /**
      * Type of car connection: platform runs directly in car but with mocked vehicle hal.
      * This will only happen in testing environment.
@@ -89,7 +91,8 @@
 
     /** @hide */
     @IntDef({CONNECTION_TYPE_EMULATOR, CONNECTION_TYPE_USB, CONNECTION_TYPE_WIFI,
-        CONNECTION_TYPE_ON_DEVICE_EMULATOR, CONNECTION_TYPE_ADB_EMULATOR, CONNECTION_TYPE_EMBEDDED})
+        CONNECTION_TYPE_ON_DEVICE_EMULATOR, CONNECTION_TYPE_ADB_EMULATOR,
+            CONNECTION_TYPE_EMBEDDED, CONNECTION_TYPE_UNKNOWN})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ConnectionType {}
 
diff --git a/car-support-lib/src/android/support/car/CarAppContextManager.java b/car-support-lib/src/android/support/car/CarAppContextManager.java
index 70616b9..64198eb 100644
--- a/car-support-lib/src/android/support/car/CarAppContextManager.java
+++ b/car-support-lib/src/android/support/car/CarAppContextManager.java
@@ -42,7 +42,7 @@
         /**
          * Lost ownership for the context, which happens when other app has set the context.
          * The app losing context should stop the action associated with the context.
-         * For example, navigaiton app currently running active navigation should stop navigation
+         * For example, navigation app currently running active navigation should stop navigation
          * upon getting this for {@link CarAppContextManager#APP_CONTEXT_NAVIGATION}.
          * @param context
          */
diff --git a/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java b/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
index a610fe4..cd145ac 100644
--- a/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
+++ b/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
@@ -34,7 +34,7 @@
      */
     CarAppContextManagerEmbedded(Object manager) {
         mManager = (android.car.CarAppContextManager) manager;
-        mOwnershipListeners = new HashMap<Integer, AppContextOwnershipChangeListenerProxy>();
+        mOwnershipListeners = new HashMap<>();
     }
 
     @Override
diff --git a/car-support-lib/src/android/support/car/CarInfoManager.java b/car-support-lib/src/android/support/car/CarInfoManager.java
index 78eacac..b40f7d3 100644
--- a/car-support-lib/src/android/support/car/CarInfoManager.java
+++ b/car-support-lib/src/android/support/car/CarInfoManager.java
@@ -54,22 +54,50 @@
     @ValueTypeDef(type = String.class)
     public static final String KEY_VEHICLE_ID = "vehicle-id";
 
+    /** Manufacturer of the head unit.*/
+    @ValueTypeDef(type = String.class)
+    public static final String KEY_HEAD_UNIT_MAKE = "headUnitMake";
+    /** Model of the head unit.*/
+    @ValueTypeDef(type = String.class)
+    public static final String KEY_HEAD_UNIT_MODEL = "headUnitModel";
+    /** Head Unit software build */
+    @ValueTypeDef(type = String.class)
+    public static final String KEY_HEAD_UNIT_SOFTWARE_BUILD = "headUnitSoftwareBuild";
+    /** Head Unit software version */
+    @ValueTypeDef(type = String.class)
+    public static final String KEY_HEAD_UNIT_SOFTWARE_VERSION = "headUnitSoftwareVersion";
+    /** Where is the driver's seat.  One of the DRIVER_SIDE_* constants */
+    @ValueTypeDef(type = Integer.class)
+    public static final String KEY_DRIVER_POSITION = "driverPosition";
+
+    /** Location of the driver: left */
+    public static final int DRIVER_SIDE_LEFT   = 0;
+    /** Location of the driver: right */
+    public static final int DRIVER_SIDE_RIGHT  = 1;
+    /** Location of the driver: center */
+    public static final int DRIVER_SIDE_CENTER = 2;
+
     /**
-     * Retrieve floating point information for car.
-     * @param key
-     * @return null if the key is not supported.
-     * @throws CarNotConnectedException
-     * @throws IllegalArgumentException
+     * Returns the value for the given key or null if the key is not supported.
      */
     public abstract Float getFloat(String key)
             throws CarNotConnectedException, IllegalArgumentException;
 
+    /**
+     * Returns the value for the given key or null if the key is not supported.
+     */
     public abstract Integer getInt(String key)
             throws CarNotConnectedException, IllegalArgumentException;
 
+    /**
+     * Returns the value for the given key or null if the key is not supported.
+     */
     public abstract Long getLong(String key)
             throws CarNotConnectedException, IllegalArgumentException;
 
+    /**
+     * Returns the value for the given key or null if the key is not supported.
+     */
     public abstract String getString(String key)
             throws CarNotConnectedException, IllegalArgumentException;
 
@@ -78,9 +106,6 @@
      * defined only for the car vendor. Vendor extension can be used for other APIs like
      * getInt / getString, but this is for passing more complex data.
      * @param key
-     * @return
-     * @throws CarNotConnectedException
-     * @throws IllegalArgumentException
      * @hide
      */
     public abstract Bundle getBundle(String key)
diff --git a/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java b/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
index fe0338e..f22b95a 100644
--- a/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
+++ b/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
@@ -79,7 +79,9 @@
 
     @Override
     public int getCarConnectionType() throws CarNotConnectedException {
-        return mCar.getCarConnectionType();
+        @android.support.car.Car.ConnectionType
+        int carConnectionType = mCar.getCarConnectionType();
+        return carConnectionType;
     }
 
     @Override
@@ -113,20 +115,20 @@
         }
         // For publicly available versions, return wrapper version.
         switch (serviceName) {
-        case Car.AUDIO_SERVICE:
-            return new CarAudioManagerEmbedded(manager);
-        case Car.SENSOR_SERVICE:
-            return new CarSensorManagerEmbedded(manager);
-        case Car.INFO_SERVICE:
-            return new CarInfoManagerEmbedded(manager);
-        case Car.APP_CONTEXT_SERVICE:
-            return new CarAppContextManagerEmbedded(manager);
-        case Car.PACKAGE_SERVICE:
-            return new CarPackageManagerEmbedded(manager);
-        case Car.CAR_NAVIGATION_SERVICE:
-            return new CarNavigationManagerEmbedded(manager);
-        default:
-            return manager;
+            case Car.AUDIO_SERVICE:
+                return new CarAudioManagerEmbedded(manager);
+            case Car.SENSOR_SERVICE:
+                return new CarSensorManagerEmbedded(manager, getContext());
+            case Car.INFO_SERVICE:
+                return new CarInfoManagerEmbedded(manager);
+            case Car.APP_CONTEXT_SERVICE:
+                return new CarAppContextManagerEmbedded(manager);
+            case Car.PACKAGE_SERVICE:
+                return new CarPackageManagerEmbedded(manager);
+            case Car.CAR_NAVIGATION_SERVICE:
+                return new CarNavigationManagerEmbedded(manager);
+            default:
+                return manager;
         }
     }
 
diff --git a/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java b/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
index e4a7a06..dbffb13 100644
--- a/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
+++ b/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
@@ -16,35 +16,71 @@
 
 package android.support.car.hardware;
 
+import android.content.Context;
 import android.support.car.CarNotConnectedException;
 
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Arrays;
 import java.util.LinkedList;
+import java.util.Set;
 
 /**
  *  @hide
  */
 public class CarSensorManagerEmbedded extends CarSensorManager {
+    private static final String TAG = "CarSensorsProxy";
 
     private final android.car.hardware.CarSensorManager mManager;
+    private final CarSensorsProxy mCarSensorsProxy;
     private final LinkedList<CarSensorEventListenerProxy> mListeners = new LinkedList<>();
 
-    public CarSensorManagerEmbedded(Object manager) {
+    public CarSensorManagerEmbedded(Object manager, Context context) {
         mManager = (android.car.hardware.CarSensorManager) manager;
+        mCarSensorsProxy = new CarSensorsProxy(context);
     }
 
     @Override
     public int[] getSupportedSensors() throws CarNotConnectedException {
         try {
-            return mManager.getSupportedSensors();
+            Set<Integer> sensorsSet = new HashSet<Integer>();
+            for (Integer sensor : mManager.getSupportedSensors()) {
+                sensorsSet.add(sensor);
+            }
+            for (Integer proxySensor : mCarSensorsProxy.getSupportedSensors()) {
+                sensorsSet.add(proxySensor);
+            }
+            return toIntArray(sensorsSet);
         } catch (android.car.CarNotConnectedException e) {
             throw new CarNotConnectedException(e);
         }
     }
 
+    private static int[] toIntArray(Collection<Integer> collection) {
+        int len = collection.size();
+        int[] arr = new int[len];
+        int arrIndex = 0;
+        for (Integer item : collection) {
+            arr[arrIndex] = item;
+            arrIndex++;
+        }
+        return arr;
+    }
+
     @Override
     public boolean isSensorSupported(int sensorType) throws CarNotConnectedException {
         try {
-            return mManager.isSensorSupported(sensorType);
+            return mManager.isSensorSupported(sensorType)
+                    || mCarSensorsProxy.isSensorSupported(sensorType);
+        } catch (android.car.CarNotConnectedException e) {
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    private boolean isSensorProxied(int sensorType) throws CarNotConnectedException {
+        try {
+            return !mManager.isSensorSupported(sensorType)
+                    && mCarSensorsProxy.isSensorSupported(sensorType);
         } catch (android.car.CarNotConnectedException e) {
             throw new CarNotConnectedException(e);
         }
@@ -53,13 +89,17 @@
     @Override
     public boolean registerListener(CarSensorEventListener listener, int sensorType,
             int rate) throws CarNotConnectedException, IllegalArgumentException {
+        if (isSensorProxied(sensorType)) {
+            return mCarSensorsProxy.registerSensorListener(listener, sensorType, rate);
+        }
         CarSensorEventListenerProxy proxy = null;
         synchronized (this) {
             proxy = findListenerLocked(listener);
             if (proxy == null) {
                 proxy = new CarSensorEventListenerProxy(listener, sensorType);
+                mListeners.add(proxy);
             } else {
-                proxy.sensors |= sensorType;
+                proxy.sensors.add(sensorType);
             }
         }
         try {
@@ -72,6 +112,7 @@
     @Override
     public void unregisterListener(CarSensorEventListener listener)
             throws CarNotConnectedException {
+        mCarSensorsProxy.unregisterSensorListener(listener);
         CarSensorEventListenerProxy proxy = null;
         synchronized (this) {
             proxy = findListenerLocked(listener);
@@ -90,14 +131,15 @@
     @Override
     public void unregisterListener(CarSensorEventListener listener, int sensorType)
             throws CarNotConnectedException {
+        mCarSensorsProxy.unregisterSensorListener(listener, sensorType);
         CarSensorEventListenerProxy proxy = null;
         synchronized (this) {
             proxy = findListenerLocked(listener);
             if (proxy == null) {
                 return;
             }
-            proxy.sensors &= ~sensorType;
-            if (proxy.sensors == 0) {
+            proxy.sensors.remove(sensorType);
+            if (proxy.sensors.isEmpty()) {
                 mListeners.remove(proxy);
             }
         }
@@ -110,6 +152,9 @@
 
     @Override
     public CarSensorEvent getLatestSensorEvent(int type) throws CarNotConnectedException {
+        if (isSensorProxied(type)) {
+            return mCarSensorsProxy.getLatestSensorEvent(type);
+        }
         try {
             return convert(mManager.getLatestSensorEvent(type));
         } catch (android.car.CarNotConnectedException e) {
@@ -143,11 +188,11 @@
             android.car.hardware.CarSensorManager.CarSensorEventListener {
 
         public final CarSensorEventListener listener;
-        public int sensors;
+        public final Set<Integer> sensors = new HashSet<Integer>();
 
-        public CarSensorEventListenerProxy(CarSensorEventListener listener, int sensors) {
+        CarSensorEventListenerProxy(CarSensorEventListener listener, int sensor) {
             this.listener = listener;
-            this.sensors = sensors;
+            this.sensors.add(sensor);
         }
 
         @Override
diff --git a/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java b/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java
new file mode 100644
index 0000000..6d5acc3
--- /dev/null
+++ b/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java
@@ -0,0 +1,489 @@
+/*
+ * 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 android.support.car.hardware;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.GpsSatellite;
+import android.location.GpsStatus;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * CarSensorsProxy adds car sensors implementation for sensors that are not provided by vehicle HAL.
+ * @hide
+ */
+class CarSensorsProxy {
+    private static final String TAG = "CarSensorsProxy";
+    private static final int MSG_SENSORT_EVENT = 1;
+
+    // @GuardedBy("this")
+    private final Map<Integer, Set<CarSensorManager.CarSensorEventListener>> mListenersMultiMap;
+    private final LocationManager mLocationManager;
+    private final SensorManager mSensorManager;
+    private final Sensor mAccelerometerSensor;
+    private final Sensor mMagneticFieldSensor;
+    private final Sensor mGyroscopeSensor;
+    private final int[] mSupportedSensors;
+
+    // @GuardedBy("this")
+    private Location mLastLocation;
+    // @GuardedBy("this")
+    private GpsStatus mLastGpsStatus;
+    // @GuardedBy("this")
+    private float[] mLastAccelerometerData = new float[3];
+    // @GuardedBy("this")
+    private float[] mLastMagneticFieldData = new float[3];
+    // @GuardedBy("this")
+    private float[] mLastGyroscopeData = new float[3];
+    // @GuardedBy("this")
+    private float[] mR = new float[16];
+    // @GuardedBy("this")
+    private float[] mI = new float[16];
+    // @GuardedBy("this")
+    private float[] mOrientation = new float[3];
+    // @GuardedBy("this")
+    private long mLastLocationTime;
+    // @GuardedBy("this")
+    private long mLastGpsStatusTime;
+    // @GuardedBy("this")
+    private long mLastAccelerometerDataTime;
+    // @GuardedBy("this")
+    private long mLastMagneticFieldDataTime;
+    // @GuardedBy("this")
+    private long mLastGyroscopeDataTime;
+
+    private final GpsStatus.Listener mGpsStatusListener = new GpsStatus.Listener() {
+            @Override
+            public void onGpsStatusChanged(int event) {
+                if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
+                    synchronized (CarSensorsProxy.this) {
+                        mLastGpsStatus = mLocationManager.getGpsStatus(mLastGpsStatus);
+                        mLastGpsStatusTime = System.nanoTime();
+                    }
+                    pushSensorChanges(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE);
+                }
+            }
+        };
+
+    private final LocationListener mLocationListener = new LocationListener() {
+            @Override
+            public void onLocationChanged(Location location) {
+                synchronized (CarSensorsProxy.this) {
+                    mLastLocation = location;
+                    mLastLocationTime = System.nanoTime();
+                }
+                pushSensorChanges(CarSensorManager.SENSOR_TYPE_LOCATION);
+            }
+
+            @Override
+            public void onProviderEnabled(String provider) {
+            }
+
+            @Override
+            public void onProviderDisabled(String provider) {
+            }
+
+            @Override
+            public void onStatusChanged(String provider, int status, Bundle extras) {
+            }
+        };
+
+    private final SensorEventListener mSensorListener = new SensorEventListener() {
+            @Override
+            public void onSensorChanged(SensorEvent event) {
+                int type = event.sensor.getType();
+                synchronized (CarSensorsProxy.this) {
+                    switch (type) {
+                        case Sensor.TYPE_GYROSCOPE:
+                            System.arraycopy(event.values, 0, mLastGyroscopeData, 0, 3);
+                            mLastGyroscopeDataTime = System.nanoTime();
+                            pushSensorChanges(CarSensorManager.SENSOR_TYPE_GYROSCOPE);
+                            break;
+                        case Sensor.TYPE_MAGNETIC_FIELD:
+                            System.arraycopy(event.values, 0, mLastMagneticFieldData, 0, 3);
+                            mLastMagneticFieldDataTime = System.nanoTime();
+                            pushSensorChanges(CarSensorManager.SENSOR_TYPE_COMPASS);
+                            break;
+                        case Sensor.TYPE_ACCELEROMETER:
+                            System.arraycopy(event.values, 0, mLastAccelerometerData, 0, 3);
+                            mLastAccelerometerDataTime = System.nanoTime();
+                            pushSensorChanges(CarSensorManager.SENSOR_TYPE_ACCELEROMETER);
+                            pushSensorChanges(CarSensorManager.SENSOR_TYPE_COMPASS);
+                            break;
+                        default:
+                            Log.w(TAG, "Unexpected sensor event type: " + type);
+                            // Should never happen.
+                            return;
+                    }
+                }
+            }
+
+            @Override
+            public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+        };
+
+    private final Handler mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_SENSORT_EVENT:
+                        int sensorType = msg.arg1;
+                        Collection<CarSensorManager.CarSensorEventListener> listenersCollection;
+                        synchronized (CarSensorsProxy.this) {
+                            listenersCollection = mListenersMultiMap.get(sensorType);
+                        }
+                        CarSensorEvent event = (CarSensorEvent) msg.obj;
+                        if (event != null) {
+                            for (CarSensorManager.CarSensorEventListener listener :
+                                         listenersCollection) {
+                                listener.onSensorChanged(event);
+                            }
+                        }
+                        break;
+                    default:
+                        Log.w(TAG, "Unexpected msg dispatched. msg: " + msg);
+                        super.handleMessage(msg);
+                }
+            }
+        };
+
+    CarSensorsProxy(Context context) {
+        mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+        mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        mMagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        mGyroscopeSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+        mListenersMultiMap = new HashMap<Integer, Set<CarSensorManager.CarSensorEventListener>>();
+        mSupportedSensors = initSupportedSensors(context);
+
+    }
+
+    private int[] initSupportedSensors(Context context) {
+        Set<Integer> features = new HashSet<>();
+        PackageManager packageManager = context.getPackageManager();
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_COMPASS)
+                && packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)) {
+            features.add(CarSensorManager.SENSOR_TYPE_COMPASS);
+        }
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)) {
+            features.add(CarSensorManager.SENSOR_TYPE_ACCELEROMETER);
+        }
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE)) {
+            features.add(CarSensorManager.SENSOR_TYPE_GYROSCOPE);
+        }
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION)) {
+            features.add(CarSensorManager.SENSOR_TYPE_LOCATION);
+            features.add(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE);
+        }
+        return toIntArray(features);
+    }
+
+    private static int[] toIntArray(Collection<Integer> collection) {
+        int len = collection.size();
+        int[] arr = new int[len];
+        int arrIndex = 0;
+        for (Integer item : collection) {
+            arr[arrIndex] = item;
+            arrIndex++;
+        }
+        return arr;
+    }
+
+
+    public boolean isSensorSupported(int sensorType) {
+        for (int sensor : mSupportedSensors) {
+            if (sensor == sensorType) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public int[] getSupportedSensors() {
+        return mSupportedSensors;
+    }
+
+    public boolean registerSensorListener(CarSensorManager.CarSensorEventListener listener,
+            int sensorType, int rate) {
+        // current implementation ignores rate.
+        boolean sensorSetChanged = false;
+        synchronized (this) {
+            if (mListenersMultiMap.get(sensorType) == null) {
+                mListenersMultiMap.put(sensorType,
+                                       new HashSet<CarSensorManager.CarSensorEventListener>());
+                sensorSetChanged = true;
+            }
+            mListenersMultiMap.get(sensorType).add(listener);
+        }
+        if (sensorSetChanged) {
+            updateSensorListeners();
+        }
+        return true;
+    }
+
+    public void unregisterSensorListener(CarSensorManager.CarSensorEventListener listener,
+            int sensorType) {
+        if (listener == null) {
+            return;
+        }
+        boolean sensorSetChanged = false;
+        synchronized (this) {
+            Set<CarSensorManager.CarSensorEventListener> sensorTypeListeneres =
+                    mListenersMultiMap.get(sensorType);
+            if (sensorTypeListeneres != null) {
+                sensorTypeListeneres.remove(listener);
+                if (sensorTypeListeneres.isEmpty()) {
+                    mListenersMultiMap.remove(sensorType);
+                    sensorSetChanged = true;
+                }
+            }
+        }
+        if (sensorSetChanged) {
+            updateSensorListeners();
+        };
+    }
+
+    public void unregisterSensorListener(CarSensorManager.CarSensorEventListener listener) {
+        if (listener == null) {
+            return;
+        }
+        boolean sensorSetChanged = false;
+        synchronized (this) {
+            for (Map.Entry<Integer, Set<CarSensorManager.CarSensorEventListener>> entry :
+                         mListenersMultiMap.entrySet()) {
+                if (entry.getValue().contains(listener)) {
+                    entry.getValue().remove(listener);
+                }
+                if (entry.getValue().isEmpty()) {
+                    sensorSetChanged = true;
+                    mListenersMultiMap.remove(entry.getKey());
+                }
+            }
+        }
+        if (sensorSetChanged) {
+            updateSensorListeners();
+        };
+    }
+
+    public CarSensorEvent getLatestSensorEvent(int type) {
+        return getSensorEvent(type);
+    }
+
+    private void pushSensorChanges(int sensorType) {
+        CarSensorEvent event = getSensorEvent(sensorType);
+        if (event == null) {
+            return;
+        }
+        Message msg = mHandler.obtainMessage(MSG_SENSORT_EVENT, sensorType, 0, event);
+        mHandler.sendMessage(msg);
+    }
+
+    private CarSensorEvent getSensorEvent(int sensorType) {
+        CarSensorEvent event = null;
+        synchronized (this) {
+            switch (sensorType) {
+                case CarSensorManager.SENSOR_TYPE_COMPASS:
+                    if (mLastMagneticFieldDataTime != 0 && mLastAccelerometerDataTime != 0) {
+                        event = new CarSensorEvent(sensorType, Math.max(mLastMagneticFieldDataTime,
+                                mLastAccelerometerDataTime), 3, 0);
+                        SensorManager.getRotationMatrix(mR, mI, mLastAccelerometerData,
+                                mLastMagneticFieldData);
+                        SensorManager.getOrientation(mR, mOrientation);
+                        event.floatValues[CarSensorEvent.INDEX_COMPASS_BEARING] =
+                                (float) Math.toDegrees(mOrientation[0]);
+                        event.floatValues[CarSensorEvent.INDEX_COMPASS_PITCH] =
+                                (float) Math.toDegrees(mOrientation[1]);
+                        event.floatValues[CarSensorEvent.INDEX_COMPASS_ROLL] =
+                                (float) Math.toDegrees(mOrientation[2]);
+                    }
+                    break;
+                case CarSensorManager.SENSOR_TYPE_LOCATION:
+                    if (mLastLocationTime != 0) {
+                        event = new CarSensorEvent(sensorType, mLastLocationTime, 6, 3);
+                        populateLocationCarSensorEvent(event, mLastLocation);
+                    }
+                    break;
+                case CarSensorManager.SENSOR_TYPE_ACCELEROMETER:
+                    if (mLastAccelerometerDataTime != 0) {
+                        event = new CarSensorEvent(sensorType, mLastAccelerometerDataTime, 3, 0);
+                        event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_X] =
+                                mLastAccelerometerData[0];
+                        event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_Y] =
+                                mLastAccelerometerData[1];
+                        event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_Z] =
+                                mLastAccelerometerData[2];
+                    }
+                    break;
+                case CarSensorManager.SENSOR_TYPE_GPS_SATELLITE:
+                    if (mLastGpsStatusTime != 0) {
+                        event = createGpsStatusCarSensorEvent(mLastGpsStatus);
+                    }
+                    break;
+                case CarSensorManager.SENSOR_TYPE_GYROSCOPE:
+                    if (mLastGyroscopeDataTime != 0) {
+                        event = new CarSensorEvent(sensorType, mLastGyroscopeDataTime, 3, 0);
+                        event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_X] = mLastGyroscopeData[0];
+                        event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_Y] = mLastGyroscopeData[1];
+                        event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_Z] = mLastGyroscopeData[2];
+                    }
+                    break;
+                default:
+                    // Should not happen.
+                    Log.w(TAG, "[getSensorEvent]: Unsupported sensor type:" + sensorType);
+                    return null;
+            }
+        }
+        return event;
+    }
+
+    private void populateLocationCarSensorEvent(CarSensorEvent event, Location location) {
+        if (location == null) {
+            return;
+        }
+        int present = 0;
+        present |= (0x1 << CarSensorEvent.INDEX_LOCATION_LONGITUDE);
+        event.intValues[CarSensorEvent.INDEX_LOCATION_LATITUDE_INTS] =
+                (int) (location.getLongitude() * 1E7);
+
+        present |= (0x1 << CarSensorEvent.INDEX_LOCATION_LATITUDE);
+        event.intValues[CarSensorEvent.INDEX_LOCATION_LATITUDE_INTS] =
+                (int) (location.getLatitude() * 1E7);
+
+        if (location.hasAccuracy()) {
+            present |= (0x1 << CarSensorEvent.INDEX_LOCATION_ACCURACY);
+            event.floatValues[CarSensorEvent.INDEX_LOCATION_ACCURACY] = location.getAccuracy();
+        }
+
+        if (location.hasAltitude()) {
+            present |= (0x1 << CarSensorEvent.INDEX_LOCATION_ALTITUDE);
+            event.floatValues[CarSensorEvent.INDEX_LOCATION_ALTITUDE] =
+                    (float) location.getAltitude();
+        }
+
+        if (location.hasSpeed()) {
+            present |= (0x1 << CarSensorEvent.INDEX_LOCATION_SPEED);
+            event.floatValues[CarSensorEvent.INDEX_LOCATION_SPEED] = location.getSpeed();
+        }
+
+        if (location.hasBearing()) {
+            present |= (0x1 << CarSensorEvent.INDEX_LOCATION_BEARING);
+            event.floatValues[CarSensorEvent.INDEX_LOCATION_BEARING] = location.getBearing();
+        }
+
+        event.intValues[0] = present;
+    }
+
+    private CarSensorEvent createGpsStatusCarSensorEvent(GpsStatus gpsStatus) {
+        CarSensorEvent event = null;
+
+        if (gpsStatus == null) {
+            return event;
+        }
+
+        int numberInView = 0;
+        int numberInUse = 0;
+        for (GpsSatellite satellite : gpsStatus.getSatellites()) {
+            ++numberInView;
+            if (satellite.usedInFix()) {
+                ++numberInUse;
+            }
+        }
+        int floatValuesSize = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_INTERVAL * numberInView
+                + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_OFFSET;
+        int intValuesSize = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_INTERVAL * numberInView
+                + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_OFFSET;
+        event = new CarSensorEvent(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE, mLastGpsStatusTime,
+                floatValuesSize, intValuesSize);
+        event.intValues[CarSensorEvent.INDEX_GPS_SATELLITE_NUMBER_IN_USE] = numberInUse;
+        event.intValues[CarSensorEvent.INDEX_GPS_SATELLITE_NUMBER_IN_VIEW] = numberInView;
+        int i = 0;
+        for (GpsSatellite satellite : gpsStatus.getSatellites()) {
+            int iInt = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_OFFSET
+                    + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_INTERVAL * i;
+            int iFloat = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_OFFSET
+                    + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_INTERVAL * i;
+            event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_PRN_OFFSET] =
+                    satellite.getPrn();
+            event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_SNR_OFFSET] =
+                    satellite.getSnr();
+            event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_AZIMUTH_OFFSET] =
+                    satellite.getAzimuth();
+            event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_ELEVATION_OFFSET] =
+                    satellite.getElevation();
+            event.intValues[iInt] = satellite.usedInFix() ? 1 : 0;
+            i++;
+        }
+        return event;
+    }
+
+    private void updateSensorListeners() {
+        Set<Integer> activeSensors;
+        synchronized (this) {
+            activeSensors = mListenersMultiMap.keySet();
+        }
+        if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_LOCATION)) {
+            mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
+                    mLocationListener);
+        } else {
+            mLocationManager.removeUpdates(mLocationListener);
+        }
+
+        if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE)) {
+            mLocationManager.addGpsStatusListener(mGpsStatusListener);
+        } else {
+            mLocationManager.removeGpsStatusListener(mGpsStatusListener);
+        }
+
+        if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_ACCELEROMETER)
+                || activeSensors.contains(CarSensorManager.SENSOR_TYPE_COMPASS)) {
+            mSensorManager.registerListener(mSensorListener, mAccelerometerSensor,
+                    SensorManager.SENSOR_DELAY_FASTEST);
+        } else {
+            mSensorManager.unregisterListener(mSensorListener, mAccelerometerSensor);
+        }
+
+        if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_COMPASS)) {
+            mSensorManager.registerListener(mSensorListener, mMagneticFieldSensor,
+                    SensorManager.SENSOR_DELAY_FASTEST);
+        } else {
+            mSensorManager.unregisterListener(mSensorListener, mMagneticFieldSensor);
+        }
+
+        if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_GYROSCOPE)) {
+            mSensorManager.registerListener(mSensorListener, mGyroscopeSensor,
+                    SensorManager.SENSOR_DELAY_FASTEST);
+        } else {
+            mSensorManager.unregisterListener(mSensorListener, mGyroscopeSensor);
+        }
+    }
+}
diff --git a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
index 003451f..de792ed 100644
--- a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
+++ b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
@@ -407,6 +407,16 @@
 }
 }
 
+public static class VehicleAudioVolumeCapabilityFlag {
+public static final int VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE = 0x1;
+public static String enumToString(int v) {
+switch(v) {
+case VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE: return "VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE";
+default: return "UNKNOWN";
+}
+}
+}
+
 public static class VehicleAudioVolumeState {
 public static final int VEHICLE_AUDIO_VOLUME_STATE_OK = 0;
 public static final int VEHICLE_AUDIO_VOLUME_STATE_LIMIT_REACHED = 1;
@@ -458,10 +468,10 @@
 }
 
 public static class VehicleAudioHwVariantConfigFlag {
-public static final int VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG = 0x1;
+public static final int VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG = 0x1;
 public static String enumToString(int v) {
 switch(v) {
-case VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG: return "VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG";
+case VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG: return "VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG";
 default: return "UNKNOWN";
 }
 }
diff --git a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
index 5dc5a6f..ac2f393 100644
--- a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
+++ b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
@@ -18,6 +18,8 @@
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
 
+import java.util.Arrays;
+
 public class VehicleNetworkProtoUtil {
     public static String VehiclePropValueToString(VehiclePropValue value) {
         StringBuilder sb = new StringBuilder();
@@ -25,6 +27,8 @@
         sb.append(Integer.toHexString(value.getProp()));
         sb.append(" type:0x");
         sb.append(Integer.toHexString(value.getValueType()));
+        sb.append(" floatValues:" + Arrays.toString(value.getFloatValuesList().toArray()));
+        sb.append(" integerValues:" + Arrays.toString(value.getInt32ValuesList().toArray()));
         return sb.toString();
     }
 
diff --git a/service/jni/Android.mk b/service/jni/Android.mk
index ed29126..012ddfa 100644
--- a/service/jni/Android.mk
+++ b/service/jni/Android.mk
@@ -32,6 +32,7 @@
 
 LOCAL_CFLAGS := \
     -Wno-unused-parameter \
+    -std=c++11
 
 LOCAL_MODULE := libjni_car_service
 LOCAL_MODULE_TAGS := optional
diff --git a/service/jni/com_android_car_CarInputService.cpp b/service/jni/com_android_car_CarInputService.cpp
index 1b02c12..a45d7e6 100644
--- a/service/jni/com_android_car_CarInputService.cpp
+++ b/service/jni/com_android_car_CarInputService.cpp
@@ -25,30 +25,42 @@
 #include <android/keycodes.h>
 #include <cutils/log.h>
 #include <utils/Errors.h>
-
+#include <unordered_map>
 
 namespace android {
 
 static int androidKeyCodeToLinuxKeyCode(int androidKeyCode) {
-    switch (androidKeyCode) {
-    case AKEYCODE_VOLUME_UP:
-        return KEY_VOLUMEUP;
-    case AKEYCODE_VOLUME_DOWN:
-        return KEY_VOLUMEDOWN;
-    case AKEYCODE_CALL:
-        return KEY_SEND;
-    case AKEYCODE_ENDCALL:
-        return KEY_END;
-    /* TODO add more keys like these:
-    case AKEYCODE_MEDIA_PLAY_PAUSE:
-    case AKEYCODE_MEDIA_STOP:
-    case AKEYCODE_MEDIA_NEXT:
-    case AKEYCODE_MEDIA_PREVIOUS:*/
-    case AKEYCODE_VOICE_ASSIST:
-        return KEY_MICMUTE;
-    default:
+    // Map Android Key Code to Linux Kernel codes
+    // according to frameworks/base/data/keyboards/Generic.kl
+
+    static const std::unordered_map<int, int> key_map {
+      { AKEYCODE_VOLUME_UP,          KEY_VOLUMEUP },
+      { AKEYCODE_VOLUME_DOWN,        KEY_VOLUMEDOWN },
+      { AKEYCODE_VOLUME_MUTE,        KEY_MUTE },
+      { AKEYCODE_CALL,               KEY_PHONE },
+      { AKEYCODE_ENDCALL,            KEY_END },  // Currently not supported in Generic.kl
+      { AKEYCODE_MUSIC,              KEY_SOUND },
+      { AKEYCODE_MEDIA_PLAY_PAUSE,   KEY_PLAYPAUSE },
+      { AKEYCODE_MEDIA_PLAY,         KEY_PLAY },
+      { AKEYCODE_BREAK,              KEY_PAUSE },
+      { AKEYCODE_MEDIA_STOP,         KEY_STOP },
+      { AKEYCODE_MEDIA_FAST_FORWARD, KEY_FASTFORWARD },
+      { AKEYCODE_MEDIA_REWIND,       KEY_REWIND },
+      { AKEYCODE_MEDIA_NEXT,         KEY_NEXTSONG },
+      { AKEYCODE_MEDIA_PREVIOUS,     KEY_PREVIOUSSONG },
+      { AKEYCODE_CHANNEL_UP,         KEY_CHANNELUP },
+      { AKEYCODE_CHANNEL_DOWN,       KEY_CHANNELDOWN },
+      { AKEYCODE_VOICE_ASSIST,       KEY_MICMUTE },
+      { AKEYCODE_HOME,               KEY_HOME }
+    };
+
+    std::unordered_map<int, int>::const_iterator got = key_map.find(androidKeyCode);
+
+    if (got == key_map.end()) {
         ALOGW("Unmapped android key code %d dropped", androidKeyCode);
         return 0;
+    } else {
+        return got->second;
     }
 }
 
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 24ef730..c722932 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -33,15 +33,18 @@
          There is no "radio" as radio routing is outside android (for external module) or same as
          music (for android internal module)
 		 OEM can put multiple policies as item and VEHICLE_PROPERTY_AUDIO_HW_VARIANT in vehicle HAL
-		 can decide which policy to use for the given H/W. This allows OEMs to support multuple
+		 can decide which policy to use for the given H/W. This allows OEMs to support multiple
 		 audio policy from single android S/W by detecting system's audio capability in
 		 vehicle HAL.-->
     <string-array translatable="false" name="audioRoutingPolicy">
         <!--  alll logical streams into single physical stream 0. -->
-        <item>"0:call,media,nav_guidance,voice_command,alarm,notification,system,safety,unknown"</item>
+        <item>"0:call,media,radio,nav_guidance,voice_command,alarm,notification,system,safety,unknown"</item>
         <!--  call and media to physical stream 0 while all others go to physical stream 1 -->
-        <item>"0:call,media,unknown#1:nav_guidance,voice_command,alarm,notification,system,safety"</item>
+        <item>"0:call,media,radio,unknown#1:nav_guidance,voice_command,alarm,notification,system,safety"</item>
     </string-array>
+    <!-- Timeout value in Ms for audio focus wait. Audio focus request not responsed within
+         this value will be treated as timeout and audio focus will be reset to LOSS state. -->
+    <integer name="audioFocusWaitTimeoutMs">1000</integer>
 
     <!-- This is kernel device node to allow input event injection for key inputs coming
          from vehicle hal -->
diff --git a/service/src/com/android/car/AudioRoutingPolicy.java b/service/src/com/android/car/AudioRoutingPolicy.java
index 39ad826..fdad5aa 100644
--- a/service/src/com/android/car/AudioRoutingPolicy.java
+++ b/service/src/com/android/car/AudioRoutingPolicy.java
@@ -44,13 +44,13 @@
     }
 
     private static int getStreamType(String str) {
-        // no radio here as radio routing is outside android (for external module) or same as music
-        // (for android internal module)
         switch (str) {
             case "call":
                 return CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL;
             case "media":
                 return CarAudioManager.CAR_AUDIO_USAGE_MUSIC;
+            case "radio":
+                return CarAudioManager.CAR_AUDIO_USAGE_RADIO;
             case "nav_guidance":
                 return CarAudioManager.CAR_AUDIO_USAGE_NAVIGATION_GUIDANCE;
             case "voice_command":
@@ -101,20 +101,8 @@
         }
         for (int i = 0; i < mPhysicalStreamForLogicalStream.length; i++) {
             if (mPhysicalStreamForLogicalStream[i] == USAGE_TYPE_INVALID) {
-                if (i == CarAudioManager.CAR_AUDIO_USAGE_RADIO) {
-                    // set radio routing to be the same as music. For external radio, this does not
-                    // matter. For internal one, it should be the same as music.
-                    int musicPhysicalStream =
-                            mPhysicalStreamForLogicalStream[CarAudioManager.CAR_AUDIO_USAGE_MUSIC];
-                    if (musicPhysicalStream == USAGE_TYPE_INVALID) {
-                        musicPhysicalStream = defaultStreamType;
-                    }
-                    mPhysicalStreamForLogicalStream[i] = musicPhysicalStream;
-                } else {
-                    Log.w(CarLog.TAG_AUDIO, "Audio routing policy did not cover logical stream " +
-                            i);
-                    mPhysicalStreamForLogicalStream[i] = defaultStreamType;
-                }
+                Log.w(CarLog.TAG_AUDIO, "Audio routing policy did not cover logical stream " + i);
+                mPhysicalStreamForLogicalStream[i] = defaultStreamType;
             }
         }
     }
diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java
index 5019fb4..2632c02 100644
--- a/service/src/com/android/car/CarAudioService.java
+++ b/service/src/com/android/car/CarAudioService.java
@@ -18,6 +18,7 @@
 import android.car.media.CarAudioManager;
 import android.car.media.ICarAudio;
 import android.content.Context;
+import android.content.res.Resources;
 import android.media.AudioAttributes;
 import android.media.AudioFocusInfo;
 import android.media.AudioManager;
@@ -30,7 +31,7 @@
 import android.util.Log;
 
 import com.android.car.hal.AudioHalService;
-import com.android.car.hal.AudioHalService.AudioHalListener;
+import com.android.car.hal.AudioHalService.AudioHalFocusListener;
 import com.android.car.hal.VehicleHal;
 import com.android.internal.annotations.GuardedBy;
 
@@ -38,9 +39,19 @@
 import java.util.LinkedList;
 
 
-public class CarAudioService extends ICarAudio.Stub implements CarServiceBase, AudioHalListener {
+public class CarAudioService extends ICarAudio.Stub implements CarServiceBase,
+        AudioHalFocusListener {
 
-    private static final long FOCUS_RESPONSE_WAIT_TIMEOUT_MS = 1000;
+    public interface AudioContextChangeListener {
+        /**
+         * Notifies the current primary audio context (app holding focus).
+         * If there is no active context, context will be 0.
+         * Will use context like CarAudioManager.CAR_AUDIO_USAGE_*
+         */
+        void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream);
+    }
+
+    private final long mFocusResponseWaitTimeoutMs;
 
     private static final String TAG_FOCUS = CarLog.TAG_AUDIO + ".FOCUS";
 
@@ -50,11 +61,12 @@
     private final Context mContext;
     private final HandlerThread mFocusHandlerThread;
     private final CarAudioFocusChangeHandler mFocusHandler;
-    private final CarAudioVolumeHandler mVolumeHandler;
     private final SystemFocusListener mSystemFocusListener;
-    private AudioPolicy mAudioPolicy;
+
     private final Object mLock = new Object();
     @GuardedBy("mLock")
+    private AudioPolicy mAudioPolicy;
+    @GuardedBy("mLock")
     private FocusState mCurrentFocusState = FocusState.STATE_LOSS;
     /** Focus state received, but not handled yet. Once handled, this will be set to null. */
     @GuardedBy("mLock")
@@ -83,6 +95,16 @@
     private boolean mCallActive = false;
     @GuardedBy("mLock")
     private int mCurrentAudioContexts = 0;
+    @GuardedBy("mLock")
+    private int mCurrentPrimaryAudioContext = 0;
+    @GuardedBy("mLock")
+    private int mCurrentPrimaryPhysicalStream = 0;
+    @GuardedBy("mLock")
+    private AudioContextChangeListener mAudioContextChangeListener;
+    @GuardedBy("mLock")
+    private CarAudioContextChangeHandler mCarAudioContextChangeHandler;
+    @GuardedBy("mLock")
+    private boolean mIsRadioExternal;
 
     private final AudioAttributes mAttributeBottom =
             CarAudioAttributesUtil.getAudioAttributesForCarUsage(
@@ -98,8 +120,10 @@
         mSystemFocusListener = new SystemFocusListener();
         mFocusHandlerThread.start();
         mFocusHandler = new CarAudioFocusChangeHandler(mFocusHandlerThread.getLooper());
-        mVolumeHandler = new CarAudioVolumeHandler(Looper.getMainLooper());
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        Resources res = context.getResources();
+        mFocusResponseWaitTimeoutMs = (long) res.getInteger(R.integer.audioFocusWaitTimeoutMs);
+
     }
 
     @Override
@@ -115,7 +139,9 @@
         if (isFocusSuported) {
             builder.setAudioPolicyFocusListener(mSystemFocusListener);
         }
-        mAudioPolicy = builder.build();
+        synchronized (mLock) {
+            mAudioPolicy = builder.build();
+        }
         if (isFocusSuported) {
             FocusState currentState = FocusState.create(mAudioHal.getCurrentFocusState());
             int r = mAudioManager.requestAudioFocus(mBottomAudioFocusHandler, mAttributeBottom,
@@ -134,9 +160,12 @@
         if (r != 0) {
             throw new RuntimeException("registerAudioPolicy failed " + r);
         }
-        mAudioHal.setListener(this);
+        mAudioHal.setFocusListener(this);
         int audioHwVariant = mAudioHal.getHwVariant();
-        mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
+        synchronized (mLock) {
+            mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
+            mIsRadioExternal = mAudioHal.isRadioExternal();
+        }
         mAudioHal.setAudioRoutingPolicy(mAudioRoutingPolicy);
         //TODO set routing policy with new AudioPolicy API. This will control which logical stream
         //     goes to which physical stream.
@@ -154,9 +183,27 @@
             mTopFocusInfo = null;
             mPendingFocusChanges.clear();
             mRadioActive = false;
+            if (mCarAudioContextChangeHandler != null) {
+                mCarAudioContextChangeHandler.cancelAll();
+                mCarAudioContextChangeHandler = null;
+            }
+            mAudioContextChangeListener = null;
+            mCurrentPrimaryAudioContext = 0;
         }
     }
 
+    public synchronized void setAudioContextChangeListener(Looper looper,
+            AudioContextChangeListener listener) {
+        if (looper == null || listener == null) {
+            throw new IllegalArgumentException("looper or listener null");
+        }
+        if (mCarAudioContextChangeHandler != null) {
+            mCarAudioContextChangeHandler.cancelAll();
+        }
+        mCarAudioContextChangeHandler = new CarAudioContextChangeHandler(looper);
+        mAudioContextChangeListener = listener;
+    }
+
     @Override
     public void dump(PrintWriter writer) {
         writer.println("*CarAudioService*");
@@ -164,6 +211,9 @@
                 " mLastFocusRequestToCar:" + mLastFocusRequestToCar);
         writer.println(" mCurrentAudioContexts:0x" + Integer.toHexString(mCurrentAudioContexts));
         writer.println(" mCallActive:" + mCallActive + " mRadioActive:" + mRadioActive);
+        writer.println(" mCurrentPrimaryAudioContext:" + mCurrentPrimaryAudioContext +
+                " mCurrentPrimaryPhysicalStream:" + mCurrentPrimaryPhysicalStream);
+        writer.println(" mIsRadioExternal:" + mIsRadioExternal);
         mAudioRoutingPolicy.dump(writer);
     }
 
@@ -178,17 +228,6 @@
     }
 
     @Override
-    public void onVolumeChange(int streamNumber, int volume, int volumeState) {
-        mVolumeHandler.handleVolumeChange(new VolumeStateChangeEvent(streamNumber, volume,
-                volumeState));
-    }
-
-    @Override
-    public void onVolumeLimitChange(int streamNumber, int volume) {
-        //TODO
-    }
-
-    @Override
     public void onStreamStatusChange(int state, int streamNumber) {
         mFocusHandler.handleStreamStateChange(state, streamNumber);
     }
@@ -333,10 +372,6 @@
                 androidFocus, flags, mAudioPolicy);
     }
 
-    private void doHandleVolumeChange(VolumeStateChangeEvent event) {
-        //TODO
-    }
-
     private void doHandleStreamStatusChange(int streamNumber, int state) {
         //TODO
     }
@@ -368,8 +403,8 @@
         return false;
     }
 
-    private boolean isFocusFromRadio(AudioFocusInfo info) {
-        if (!mAudioHal.isRadioExternal()) {
+    private boolean isFocusFromExternalRadio(AudioFocusInfo info) {
+        if (!mIsRadioExternal) {
             // if radio is not external, no special handling of radio is necessary.
             return false;
         }
@@ -430,6 +465,24 @@
         int logicalStreamTypeForTop = CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib);
         int physicalStreamTypeForTop = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
                 logicalStreamTypeForTop);
+
+        // update primary context and notify if necessary
+        int primaryContext = logicalStreamTypeForTop;
+        switch (logicalStreamTypeForTop) {
+            case CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_BOTTOM:
+            case CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY:
+                primaryContext = 0;
+                break;
+        }
+        if (mCurrentPrimaryAudioContext != primaryContext) {
+            mCurrentPrimaryAudioContext = primaryContext;
+             mCurrentPrimaryPhysicalStream = physicalStreamTypeForTop;
+            if (mCarAudioContextChangeHandler != null) {
+                mCarAudioContextChangeHandler.requestContextChangeNotification(
+                        mAudioContextChangeListener, primaryContext, physicalStreamTypeForTop);
+            }
+        }
+
         int audioContexts = 0;
         if (logicalStreamTypeForTop == CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL) {
             if (!mCallActive) {
@@ -440,7 +493,8 @@
             if (mCallActive) {
                 mCallActive = false;
             }
-            audioContexts = AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
+            audioContexts =
+                    AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
         }
         // other apps having focus
         int focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE;
@@ -448,10 +502,8 @@
         int streamsToRequest = 0x1 << physicalStreamTypeForTop;
         switch (mTopFocusInfo.getGainRequest()) {
             case AudioManager.AUDIOFOCUS_GAIN:
-                if (isFocusFromRadio(mTopFocusInfo)) {
+                if (isFocusFromExternalRadio(mTopFocusInfo)) {
                     mRadioActive = true;
-                    // audio context sending is only for audio from android.
-                    audioContexts = 0;
                 } else {
                     mRadioActive = false;
                 }
@@ -494,9 +546,9 @@
             //     Most cars do not allow that, but if mixing is possible, it can take media stream.
             //     For now, assume no mixing capability.
             int radioPhysicalStream = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
-                    CarAudioManager.CAR_AUDIO_USAGE_MUSIC);
-            if (!isFocusFromRadio(mTopFocusInfo) &&
-                    (physicalStreamTypeForTop == radioPhysicalStream)) {
+                    CarAudioManager.CAR_AUDIO_USAGE_RADIO);
+            if (!isFocusFromExternalRadio(mTopFocusInfo) &&
+                    (physicalStreamTypeForTop == radioPhysicalStream) && mIsRadioExternal) {
                 Log.i(CarLog.TAG_AUDIO, "Top stream is taking the same stream:" +
                     physicalStreamTypeForTop + " as radio, stopping radio");
                 // stream conflict here. radio cannot be played
@@ -542,7 +594,7 @@
             mAudioHal.requestAudioFocusChange(focusToRequest, streamsToRequest, extFocus,
                     audioContexts);
             try {
-                mLock.wait(FOCUS_RESPONSE_WAIT_TIMEOUT_MS);
+                mLock.wait(mFocusResponseWaitTimeoutMs);
             } catch (InterruptedException e) {
                 //ignore
             }
@@ -657,7 +709,7 @@
                 mAudioHal.requestAudioFocusChange(
                         AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE, 0, 0);
                 try {
-                    mLock.wait(FOCUS_RESPONSE_WAIT_TIMEOUT_MS);
+                    mLock.wait(mFocusResponseWaitTimeoutMs);
                 } catch (InterruptedException e) {
                     //ignore
                 }
@@ -748,6 +800,37 @@
         }
     }
 
+    private class CarAudioContextChangeHandler extends Handler {
+        private static final int MSG_CONTEXT_CHANGE = 0;
+
+        private CarAudioContextChangeHandler(Looper looper) {
+            super(looper);
+        }
+
+        private void requestContextChangeNotification(AudioContextChangeListener listener,
+                int primaryContext, int physicalStream) {
+            Message msg = obtainMessage(MSG_CONTEXT_CHANGE, primaryContext, physicalStream,
+                    listener);
+            sendMessage(msg);
+        }
+
+        private void cancelAll() {
+            removeMessages(MSG_CONTEXT_CHANGE);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CONTEXT_CHANGE: {
+                    AudioContextChangeListener listener = (AudioContextChangeListener) msg.obj;
+                    int context = msg.arg1;
+                    int physicalStream = msg.arg2;
+                    listener.onContextChange(context, physicalStream);
+                } break;
+            }
+        }
+    }
+
     private class CarAudioFocusChangeHandler extends Handler {
         private static final int MSG_FOCUS_CHANGE = 0;
         private static final int MSG_STREAM_STATE_CHANGE = 1;
@@ -815,40 +898,6 @@
         }
     }
 
-    private class CarAudioVolumeHandler extends Handler {
-        private static final int MSG_VOLUME_CHANGE = 0;
-
-        private CarAudioVolumeHandler(Looper looper) {
-            super(looper);
-        }
-
-        private void handleVolumeChange(VolumeStateChangeEvent event) {
-            Message msg = obtainMessage(MSG_VOLUME_CHANGE, event);
-            sendMessage(msg);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_VOLUME_CHANGE:
-                    doHandleVolumeChange((VolumeStateChangeEvent) msg.obj);
-                    break;
-            }
-        }
-    }
-
-    private static class VolumeStateChangeEvent {
-        public final int stream;
-        public final int volume;
-        public final int state;
-
-        public VolumeStateChangeEvent(int stream, int volume, int state) {
-            this.stream = stream;
-            this.volume = volume;
-            this.state = state;
-        }
-    }
-
     /** Wrapper class for holding the current focus state from car. */
     private static class FocusState {
         public final int focusState;
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 8b0bd3d..6340549 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -42,11 +42,13 @@
 
     private final Context mContext;
 
-    private KeyEventListener mVoiceAssitantKeyListener;
-    private KeyEventListener mLongVoiceAssitantKeyListener;
+    private KeyEventListener mVoiceAssistantKeyListener;
+    private KeyEventListener mLongVoiceAssistantKeyListener;
     private long mLastVoiceKeyDownTime = 0;
 
-    private KeyEventListener mInstumentClusterKeyListener;
+    private KeyEventListener mInstrumentClusterKeyListener;
+
+    private KeyEventListener mVolumeKeyListener;
 
     private ParcelFileDescriptor mInjectionDeviceFd;
 
@@ -62,9 +64,9 @@
      * If listener is set, short key press will lead into calling the listener.
      * @param listener
      */
-    public void setVoiceAssitantKeyListener(KeyEventListener listener) {
+    public void setVoiceAssistantKeyListener(KeyEventListener listener) {
         synchronized (this) {
-            mVoiceAssitantKeyListener = listener;
+            mVoiceAssistantKeyListener = listener;
         }
     }
 
@@ -74,15 +76,21 @@
      * If listener is set, short long press will lead into calling the listener.
      * @param listener
      */
-    public void setLongVoiceAssitantKeyListener(KeyEventListener listener) {
+    public void setLongVoiceAssistantKeyListener(KeyEventListener listener) {
         synchronized (this) {
-            mLongVoiceAssitantKeyListener = listener;
+            mLongVoiceAssistantKeyListener = listener;
         }
     }
 
     public void setInstrumentClusterKeyListener(KeyEventListener listener) {
         synchronized (this) {
-            mInstumentClusterKeyListener = listener;
+            mInstrumentClusterKeyListener = listener;
+        }
+    }
+
+    public void setVolumeKeyListener(KeyEventListener listener) {
+        synchronized (this) {
+            mVolumeKeyListener = listener;
         }
     }
 
@@ -112,9 +120,9 @@
     @Override
     public void release() {
         synchronized (this) {
-            mVoiceAssitantKeyListener = null;
-            mLongVoiceAssitantKeyListener = null;
-            mInstumentClusterKeyListener = null;
+            mVoiceAssistantKeyListener = null;
+            mLongVoiceAssistantKeyListener = null;
+            mInstrumentClusterKeyListener = null;
             if (mInjectionDeviceFd != null) {
                 try {
                     mInjectionDeviceFd.close();
@@ -136,6 +144,10 @@
             case KeyEvent.KEYCODE_VOICE_ASSIST:
                 handleVoiceAssistKey(event);
                 return;
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+                handleVolumeKey(event);
+                return;
             default:
                 break;
         }
@@ -160,12 +172,12 @@
             KeyEventListener longPressListener = null;
             long downTime;
             synchronized (this) {
-                shortPressListener = mVoiceAssitantKeyListener;
-                longPressListener = mLongVoiceAssitantKeyListener;
+                shortPressListener = mVoiceAssistantKeyListener;
+                longPressListener = mLongVoiceAssistantKeyListener;
                 downTime = mLastVoiceKeyDownTime;
             }
             if (shortPressListener == null && longPressListener == null) {
-                launchDefaultVoiceAssitantHandler();
+                launchDefaultVoiceAssistantHandler();
             } else {
                 long duration = SystemClock.elapsedRealtime() - downTime;
                 listener = (duration > VOICE_LONG_PRESS_TIME_MS
@@ -173,13 +185,13 @@
                 if (listener != null) {
                     listener.onKeyEvent(event);
                 } else {
-                    launchDefaultVoiceAssitantHandler();
+                    launchDefaultVoiceAssistantHandler();
                 }
             }
         }
     }
 
-    private void launchDefaultVoiceAssitantHandler() {
+    private void launchDefaultVoiceAssistantHandler() {
         Log.i(CarLog.TAG_INPUT, "voice key, launch default intent");
         Intent voiceIntent =
                 new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
@@ -189,7 +201,18 @@
     private void handleInstrumentClusterKey(KeyEvent event) {
         KeyEventListener listener = null;
         synchronized (this) {
-            listener = mInstumentClusterKeyListener;
+            listener = mInstrumentClusterKeyListener;
+        }
+        if (listener == null) {
+            return;
+        }
+        listener.onKeyEvent(event);
+    }
+
+    private void handleVolumeKey(KeyEvent event) {
+        KeyEventListener listener = null;
+        synchronized (this) {
+            listener = mVolumeKeyListener;
         }
         if (listener == null) {
             return;
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 66c75fa..35e6494 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -23,10 +23,7 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.Binder;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
@@ -184,9 +181,9 @@
                         CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH);
             }
         }
-        mCarInputService.setVoiceAssitantKeyListener(listenShortPress
+        mCarInputService.setVoiceAssistantKeyListener(listenShortPress
                 ? mVoiceAssistantKeyListener : null);
-        mCarInputService.setLongVoiceAssitantKeyListener(listenLongPress
+        mCarInputService.setLongVoiceAssistantKeyListener(listenLongPress
                 ? mLongVoiceAssistantKeyListener : null);
     }
 
diff --git a/service/src/com/android/car/hal/AudioHalService.java b/service/src/com/android/car/hal/AudioHalService.java
index 893fbff..d9906cd 100644
--- a/service/src/com/android/car/hal/AudioHalService.java
+++ b/service/src/com/android/car/hal/AudioHalService.java
@@ -33,7 +33,9 @@
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioRoutingPolicyIndex;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamState;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamStateIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeCapabilityFlag;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeLimitIndex;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfigs;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
@@ -126,7 +128,7 @@
     public static final int AUDIO_CONTEXT_SYSTEM_SOUND_FLAG =
             VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
 
-    public interface AudioHalListener {
+    public interface AudioHalFocusListener {
         /**
          * Audio focus change from car.
          * @param focusState
@@ -136,6 +138,15 @@
          */
         void onFocusChange(int focusState, int streams, int externalFocus);
         /**
+         * Stream state change (start / stop) from android
+         * @param streamNumber
+         * @param state
+         */
+        void onStreamStatusChange(int streamNumber, int state);
+    }
+
+    public interface AudioHalVolumeListener {
+        /**
          * Audio volume change from car.
          * @param streamNumber
          * @param volume
@@ -148,38 +159,25 @@
          * @param volume
          */
         void onVolumeLimitChange(int streamNumber, int volume);
-        /**
-         * Stream state change (start / stop) from android
-         * @param streamNumber
-         * @param state
-         */
-        void onStreamStatusChange(int streamNumber, int state);
     }
 
     private final VehicleHal mVehicleHal;
-    private AudioHalListener mListener;
+    private AudioHalFocusListener mFocusListener;
+    private AudioHalVolumeListener mVolumeListener;
     private int mVariant;
 
-    private List<VehiclePropValue> mQueuedEvents;
-
     private final HashMap<Integer, VehiclePropConfig> mProperties = new HashMap<>();
 
     public AudioHalService(VehicleHal vehicleHal) {
         mVehicleHal = vehicleHal;
     }
 
-    public void setListener(AudioHalListener listener) {
-        List<VehiclePropValue> eventsToDispatch = null;
-        synchronized (this) {
-            mListener = listener;
-            if (mQueuedEvents != null) {
-                eventsToDispatch = mQueuedEvents;
-                mQueuedEvents = null;
-            }
-        }
-        if (eventsToDispatch != null) {
-            dispatchEventToListener(listener, eventsToDispatch);
-        }
+    public synchronized void setFocusListener(AudioHalFocusListener focusListener) {
+        mFocusListener = focusListener;
+    }
+
+    public synchronized void setVolumeListener(AudioHalVolumeListener volumeListener) {
+        mVolumeListener = volumeListener;
     }
 
     public void setAudioRoutingPolicy(AudioRoutingPolicy policy) {
@@ -262,7 +260,7 @@
             return true;
         }
         return (config.getConfigArray(0) &
-                VehicleAudioHwVariantConfigFlag.VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG)
+                VehicleAudioHwVariantConfigFlag.VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG)
                 == 0;
     }
 
@@ -270,6 +268,44 @@
         return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS);
     }
 
+    public synchronized boolean isAudioVolumeSupported() {
+        return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+    }
+
+    public synchronized int getSupportedAudioVolumeContexts() {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        return config.getConfigArray(0);
+    }
+
+    /**
+     * Whether external audio module can memorize logical audio volumes or not.
+     * @return
+     */
+    public synchronized boolean isExternalAudioVolumePersistent() {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        if (config.getConfigArray(0) == 0) { // physical streams only
+            return false;
+        }
+        if ((config.getConfigArray(1) &
+                VehicleAudioVolumeCapabilityFlag.VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE)
+                != 0) {
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized boolean isAudioVolumeLimitSupported() {
+        return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT);
+    }
+
     /**
      * Get the current audio focus state.
      * @return 0: focusState, 1: streams, 2: externalFocus
@@ -340,22 +376,18 @@
 
     @Override
     public void handleHalEvents(List<VehiclePropValue> values) {
-        AudioHalListener listener = null;
+        AudioHalFocusListener focusListener = null;
+        AudioHalVolumeListener volumeListener = null;
         synchronized (this) {
-            listener = mListener;
-            if (listener == null) {
-                if (mQueuedEvents == null) {
-                    mQueuedEvents = new LinkedList<VehiclePropValue>();
-                }
-                mQueuedEvents.addAll(values);
-            }
+            focusListener = mFocusListener;
+            volumeListener = mVolumeListener;
         }
-        if (listener != null) {
-            dispatchEventToListener(listener, values);
-        }
+        dispatchEventToListener(focusListener, volumeListener, values);
     }
 
-    private void dispatchEventToListener(AudioHalListener listener, List<VehiclePropValue> values) {
+    private void dispatchEventToListener(AudioHalFocusListener focusListener,
+            AudioHalVolumeListener volumeListener,
+            List<VehiclePropValue> values) {
         for (VehiclePropValue v : values) {
             switch (v.getProp()) {
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS: {
@@ -365,7 +397,18 @@
                             VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_STREAMS);
                     int externalFocus = v.getInt32Values(
                             VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_EXTERNAL_FOCUS_STATE);
-                    listener.onFocusChange(focusState, streams, externalFocus);
+                    if (focusListener != null) {
+                        focusListener.onFocusChange(focusState, streams, externalFocus);
+                    }
+                } break;
+                case VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE: {
+                    int state = v.getInt32Values(
+                            VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STATE);
+                    int streamNum = v.getInt32Values(
+                            VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STREAM);
+                    if (focusListener != null) {
+                        focusListener.onStreamStatusChange(streamNum, state);
+                    }
                 } break;
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME: {
                     int volume = v.getInt32Values(
@@ -374,20 +417,22 @@
                             VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STREAM);
                     int volumeState = v.getInt32Values(
                             VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STATE);
-                    listener.onVolumeChange(streamNum, volume, volumeState);
+                    if (volumeListener != null) {
+                        volumeListener.onVolumeChange(streamNum, volume, volumeState);
+                    }
                 } break;
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT: {
-                    //TODO
-                } break;
-                case VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE: {
-                    int state = v.getInt32Values(
-                            VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STATE);
-                    int streamNum = v.getInt32Values(
-                            VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STREAM);
-                    listener.onStreamStatusChange(streamNum, state);
+                    int stream = v.getInt32Values(
+                            VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_STREAM);
+                    int maxVolume = v.getInt32Values(
+                            VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_MAX_VOLUME);
+                    if (volumeListener != null) {
+                        volumeListener.onVolumeLimitChange(stream, maxVolume);
+                    }
                 } break;
             }
         }
+        values.clear();
     }
 
     @Override
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
index f347ce9..e730b03 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
@@ -24,24 +24,11 @@
         android:orientation="vertical"
         android:layout_weight="1" />
     <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        android:layout_weight="1" >
-        <Button
-            android:id="@+id/button_volume_up"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/volume_up" />
-        <Button
-            android:id="@+id/button_volume_down"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/volume_down" />
-        <Button
-            android:id="@+id/button_voice"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/voice" />
+            android:orientation="vertical"
+            android:layout_weight="1"
+            android:id="@+id/input_buttons">
+        <!-- Filled at runtime. -->
     </LinearLayout>
 </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
index c617094..7eac97c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
@@ -20,6 +20,12 @@
     android:layout_marginLeft="96dp">
 
     <TextView
+        android:id="@+id/driving_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="20dp"/>
+
+    <TextView
         android:id="@+id/search_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml b/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml
new file mode 100644
index 0000000..cef8754
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical" >
+    <!--  dummy one for top area -->
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:orientation="vertical"
+            android:layout_weight="1" />
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_weight="1" >
+        <Button
+                android:id="@+id/button_open_radio"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_open" />
+        <Button
+                android:id="@+id/button_close_radio"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_close" />
+    </LinearLayout>
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_weight="1" >
+        <Button
+                android:id="@+id/button_get_focus_in_radio"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_get_focus" />
+        <Button
+                android:id="@+id/button_release_focus_in_radio"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_release_focus" />
+    </LinearLayout>
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_weight="1" >
+        <ToggleButton
+                android:id="@+id/button_band_selection"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checked="true"
+                android:textOn="@string/radio_fm"
+                android:textOff="@string/radio_am" />
+        <Button
+                android:id="@+id/button_radio_next"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_next" />
+        <Button
+                android:id="@+id/button_radio_prev"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_prev" />
+        <Button
+                android:id="@+id/button_radio_scan_cancel"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_scan_cancel" />
+    </LinearLayout>
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_weight="1" >
+        <TextView
+                android:id="@+id/radio_station_info"
+                android:layout_marginRight="@dimen/radioInfoMargin"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        <TextView
+                android:id="@+id/radio_channel_info"
+                android:layout_marginRight="@dimen/radioInfoMargin"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        <TextView
+                android:id="@+id/radio_song_info"
+                android:layout_marginRight="@dimen/radioInfoMargin"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        <TextView
+                android:id="@+id/radio_artist_info"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+    </LinearLayout>
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_weight="1" >
+        <TextView
+                android:id="@+id/radio_log"
+                android:maxLines="@integer/radio_log_lines"
+                android:scrollbars="vertical"
+                android:gravity="bottom"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
index e35ee3d..17b9dbe 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
@@ -18,4 +18,5 @@
     <dimen name="rvcBtnWidth">150dp</dimen>
     <dimen name="rvcTextSize">10dp</dimen>
     <dimen name="rvcTvHeight">80dp</dimen>
+    <dimen name="radioInfoMargin">5dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/integers.xml b/tests/EmbeddedKitchenSinkApp/res/values/integers.xml
new file mode 100644
index 0000000..e8b418f
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/values/integers.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+    <integer name="radio_log_lines">5</integer>
+</resources>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 9eeeaa9..0861351 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -105,5 +105,35 @@
     <!--  input test -->
     <string name="volume_up">Volume +</string>
     <string name="volume_down">Volume -</string>
+    <string name="volume_mute">Mute</string>
     <string name="voice">Voice</string>
+    <string name="mock_vehicle_hal">Mock HAL</string>
+    <string name="mock_vehicle_hal_off">Mock HAL OFF</string>
+    <string name="mock_vehicle_hal_on">Mock HAL ON</string>
+    <string name="music">Music</string>
+    <string name="call_send">Call</string>
+    <string name="call_end">Call end</string>
+    <string name="home">Home</string>
+    <string name="next_song">Next song</string>
+    <string name="prev_song">Prev song</string>
+    <string name="tune_right">Tune +</string>
+    <string name="tune_left">Tune -</string>
+    <string name="music_play">Play</string>
+    <string name="music_stop">Stop</string>
+
+    <!-- radio test -->
+    <string name="radio_open">Open</string>
+    <string name="radio_close">Close</string>
+    <string name="radio_get_focus">Get Audio focus</string>
+    <string name="radio_release_focus">Release Audio focus</string>
+    <string name="radio_next">Next</string>
+    <string name="radio_prev">Previous</string>
+    <string name="radio_scan_cancel">Cancel scan</string>
+    <string name="radio_am">AM</string>
+    <string name="radio_fm">FM</string>
+    <string name="radio_station_info">Station info: %1$s</string>
+    <string name="radio_channel_info">Channel info: %1$s kHz</string>
+    <string name="radio_song_info">Song info: %1$s</string>
+    <string name="radio_artist_info">Artist info: %1$s</string>
+    <string name="radio_na">N/A</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index d227486..88270da 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -44,6 +44,7 @@
 import com.google.android.car.kitchensink.input.InputTestFragment;
 import com.google.android.car.kitchensink.job.JobSchedulerFragment;
 import com.google.android.car.kitchensink.keyboard.KeyboardFragment;
+import com.google.android.car.kitchensink.radio.RadioTestFragment;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,6 +60,7 @@
     private static final String MENU_KEYBOARD = "keyboard";
     private static final String MENU_CLUSTER = "inst cluster";
     private static final String MENU_INPUT_TEST = "input test";
+    private static final String MENU_RADIO = "radio";
 
     private Car mCarApi;
     private CarCameraManager mCameraManager;
@@ -69,6 +71,7 @@
 
 
     private AudioTestFragment mAudioTestFragment;
+    private RadioTestFragment mRadioTestFragment;
     private CameraTestFragment mCameraTestFragment;
     private HvacTestFragment mHvacTestFragment;
     private JobSchedulerFragment mJobFragment;
@@ -213,8 +216,8 @@
             List<CarMenu.Item> items = new ArrayList<>();
             if (parentId.equals(ROOT)) {
                 String[] allMenus = {
-                        MENU_AUDIO, MENU_CAMERA, MENU_HVAC, MENU_JOB, MENU_KEYBOARD, MENU_CLUSTER,
-                        MENU_INPUT_TEST, MENU_QUIT
+                        MENU_AUDIO, MENU_RADIO, MENU_CAMERA, MENU_HVAC, MENU_JOB, MENU_KEYBOARD,
+                        MENU_CLUSTER, MENU_INPUT_TEST, MENU_QUIT
                 };
                 for (String menu : allMenus) {
                     items.add(new CarMenu.Builder(menu).setText(menu).build());
@@ -231,6 +234,11 @@
                     mAudioTestFragment = new AudioTestFragment();
                 }
                 setContentFragment(mAudioTestFragment);
+            } else if (id.equals(MENU_RADIO)) {
+                if (mRadioTestFragment == null) {
+                    mRadioTestFragment = new RadioTestFragment();
+                }
+                setContentFragment(mRadioTestFragment);
             } else if (id.equals(MENU_CAMERA)) {
                 if (mCameraManager != null) {
                     if (mCameraTestFragment == null) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
index 64ede01..a177c00 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
@@ -16,6 +16,7 @@
 package com.google.android.car.kitchensink.input;
 
 import android.annotation.Nullable;
+import android.annotation.StringRes;
 import android.car.Car;
 import android.car.CarNotConnectedException;
 import android.car.test.CarTestManager;
@@ -34,6 +35,17 @@
 import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.google.android.car.kitchensink.R;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHwKeyInputAction;
+import com.android.car.vehiclenetwork.VehiclePropValueUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 import com.android.car.vehiclenetwork.VehicleNetworkConsts;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHwKeyInputAction;
@@ -49,11 +61,11 @@
 
     private static final String TAG = "CAR.INPUT.KS";
 
+    private static final Button BREAK_LINE = null;
+
     private Car mCar;
     private CarTestManager mTestManager;
-    private Button mVolumeUp;
-    private Button mVolumeDown;
-    private Button mVoice;
+    private final List<View> mButtons = new ArrayList<>();
 
     @Nullable
     @Override
@@ -61,32 +73,27 @@
             @Nullable Bundle savedInstanceState) {
         View view = inflater.inflate(R.layout.input_test, container, false);
 
-        // Single touch + key event does not work as touch is happening in other window
-        // at the same time. But long press will work.
-        mVolumeUp = (Button) view.findViewById(R.id.button_volume_up);
-        mVolumeUp.setOnTouchListener(new OnTouchListener() {
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                handleTouchEvent(event, KeyEvent.KEYCODE_VOLUME_UP);
-                return true;
-            }
-        });
-        mVolumeDown = (Button) view.findViewById(R.id.button_volume_down);
-        mVolumeDown.setOnTouchListener(new OnTouchListener() {
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                handleTouchEvent(event, KeyEvent.KEYCODE_VOLUME_DOWN);
-                return true;
-            }
-        });
-        mVoice = (Button) view.findViewById(R.id.button_voice);
-        mVoice.setOnTouchListener(new OnTouchListener() {
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                handleTouchEvent(event, KeyEvent.KEYCODE_VOICE_ASSIST);
-                return true;
-            }
-        });
+        Collections.addAll(mButtons,
+                BREAK_LINE,
+                createButton(R.string.home, KeyEvent.KEYCODE_HOME),
+                createButton(R.string.volume_up, KeyEvent.KEYCODE_VOLUME_UP),
+                createButton(R.string.volume_down, KeyEvent.KEYCODE_VOLUME_DOWN),
+                createButton(R.string.volume_mute, KeyEvent.KEYCODE_VOLUME_MUTE),
+                createButton(R.string.voice, KeyEvent.KEYCODE_VOICE_ASSIST),
+                BREAK_LINE,
+                createButton(R.string.music, KeyEvent.KEYCODE_MUSIC),
+                createButton(R.string.music_play, KeyEvent.KEYCODE_MEDIA_PLAY),
+                createButton(R.string.music_stop, KeyEvent.KEYCODE_MEDIA_STOP),
+                createButton(R.string.next_song, KeyEvent.KEYCODE_MEDIA_NEXT),
+                createButton(R.string.prev_song, KeyEvent.KEYCODE_MEDIA_PREVIOUS),
+                createButton(R.string.tune_right, KeyEvent.KEYCODE_CHANNEL_UP),
+                createButton(R.string.tune_left, KeyEvent.KEYCODE_CHANNEL_DOWN),
+                BREAK_LINE,
+                createButton(R.string.call_send, KeyEvent.KEYCODE_CALL),
+                createButton(R.string.call_end, KeyEvent.KEYCODE_ENDCALL)
+                );
+
+        addButtonsToPanel((LinearLayout) view.findViewById(R.id.input_buttons), mButtons);
 
         mCar = Car.createCar(getContext(), new ServiceConnection() {
             @Override
@@ -97,12 +104,16 @@
                 } catch (CarNotConnectedException e) {
                     throw new RuntimeException("Failed to create test service manager", e);
                 }
-                if (!mTestManager.isPropertySupported(
-                        VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT)) {
+                boolean hwKeySupported = mTestManager.isPropertySupported(
+                        VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT);
+                if (!hwKeySupported) {
                     Log.w(TAG, "VEHICLE_PROPERTY_HW_KEY_INPUT not supported");
-                    mVolumeUp.setEnabled(false);
-                    mVolumeDown.setEnabled(false);
-                    mVoice.setEnabled(false);
+                }
+
+                for (View v : mButtons) {
+                    if (v != null) {
+                        v.setEnabled(hwKeySupported);
+                    }
                 }
             }
 
@@ -114,6 +125,34 @@
         return view;
     }
 
+    private Button createButton(@StringRes int textResId, int keyCode) {
+        Button button = new Button(getContext());
+        button.setText(getContext().getString(textResId));
+        button.setTextSize(32f);
+        // Single touch + key event does not work as touch is happening in other window
+        // at the same time. But long press will work.
+        button.setOnTouchListener((v, event) -> {
+            handleTouchEvent(event, keyCode);
+            return true;
+        });
+
+        return button;
+    }
+
+    private void checkHwKeyInputSupported() {
+        boolean hwKeyInputSupported = mTestManager.isPropertySupported(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT);
+        if (!hwKeyInputSupported) {
+            Log.w(TAG, "VEHICLE_PROPERTY_HW_KEY_INPUT not supported");
+        }
+
+        for (View v : mButtons) {
+            if (v != null) {
+                v.setEnabled(hwKeyInputSupported);
+            }
+        }
+    }
+
     private void handleTouchEvent(MotionEvent event, int keyCode) {
         int action = event.getActionMasked();
         Log.i(TAG, "handleTouchEvent, action:" + action + ",keyCode:" + keyCode);
@@ -139,4 +178,17 @@
         super.onDestroyView();
         mCar.disconnect();
     }
+
+    private void addButtonsToPanel(LinearLayout root, List<View> buttons) {
+        LinearLayout panel = null;
+        for (View button : buttons) {
+            if (button == BREAK_LINE) {
+                panel = new LinearLayout(getContext());
+                panel.setOrientation(LinearLayout.HORIZONTAL);
+                root.addView(panel);
+            } else {
+                panel.addView(button);
+            }
+        }
+    }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
index 4ee0b16..42fc6d7 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
@@ -18,9 +18,15 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.annotation.Nullable;
+import android.support.car.Car;
+import android.support.car.CarNotConnectedException;
+import android.support.car.CarNotSupportedException;
 import android.support.car.app.menu.CarDrawerActivity;
 import android.support.car.app.menu.SearchBoxEditListener;
+import android.support.car.hardware.CarSensorEvent;
+import android.support.car.hardware.CarSensorManager;
 import android.support.v4.app.Fragment;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -30,16 +36,19 @@
 import com.google.android.car.kitchensink.R;
 
 public class KeyboardFragment extends Fragment {
+    private static final String TAG = "KitchenSinkKeyboard";
     public static final int CARD = 0xfffafafa;
     public static final int TEXT_PRIMARY_DAY = 0xde000000;
     public static final int TEXT_SECONDARY_DAY = 0x8a000000;
 
+    private TextView mDrivingStatus;
     private Button mImeButton;
     private Button mCloseImeButton;
     private Button mShowHideInputButton;
     private CarDrawerActivity mActivity;
     private TextView mOnSearchText;
     private TextView mOnEditText;
+    private CarSensorManager mSensorManager;
 
     private final Handler mHandler = new Handler();
 
@@ -81,10 +90,53 @@
         mOnEditText = (TextView) v.findViewById(R.id.edit_text);
         resetInput();
         mActivity.setSearchBoxEndView(View.inflate(getContext(), R.layout.keyboard_end_view, null));
-
+        mDrivingStatus = (TextView) v.findViewById(R.id.driving_status);
         return v;
     }
 
+    @Override
+    public void onResume() {
+        super.onResume();
+        try {
+            mSensorManager = (CarSensorManager)
+                    mActivity.getCar().getCarManager(Car.SENSOR_SERVICE);
+            mSensorManager.registerListener(mCarSensorListener,
+                    CarSensorManager.SENSOR_TYPE_DRIVING_STATUS,
+                    CarSensorManager.SENSOR_RATE_FASTEST);
+        } catch (CarNotSupportedException | CarNotConnectedException e) {
+            Log.e(TAG, "Car not connected or not supported", e);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mSensorManager != null) {
+            mSensorManager.unregisterListener(mCarSensorListener);
+        }
+    }
+
+    private final CarSensorManager.CarSensorEventListener mCarSensorListener =
+            new CarSensorManager.CarSensorEventListener() {
+                @Override
+                public void onSensorChanged(CarSensorEvent event) {
+                    if (event.sensorType != CarSensorManager.SENSOR_TYPE_DRIVING_STATUS) {
+                        return;
+                    }
+                    int drivingStatus = event.getDrivingStatusData(null).status;
+
+                    boolean keyboardEnabled =
+                            (drivingStatus & CarSensorEvent.DRIVE_STATUS_NO_KEYBOARD_INPUT) == 0;
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mDrivingStatus.setText("Driving status: " + drivingStatus
+                                    + " Keyboard " + (keyboardEnabled ? "enabled" : "disabled"));
+                        }
+                    });
+                }
+            };
+
     private void resetInput() {
         mActivity.showSearchBox(new View.OnClickListener() {
             @Override
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java
new file mode 100644
index 0000000..c812b7b
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java
@@ -0,0 +1,420 @@
+/*
+ * 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.google.android.car.kitchensink.radio;
+
+import android.annotation.Nullable;
+import android.car.Car;
+import android.car.media.CarAudioManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import com.google.android.car.kitchensink.R;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class RadioTestFragment extends Fragment {
+    private static final String TAG = "CAR.RADIO.KS";
+    private static final boolean DBG = true;
+    private static final int MAX_LOG_MESSAGES = 100;
+
+    private final AudioManager.OnAudioFocusChangeListener mRadioFocusListener =
+            new AudioManager.OnAudioFocusChangeListener() {
+                @Override
+                public void onAudioFocusChange(int focusChange) {
+                    addLog(Log.INFO, "Radio focus change:" + focusChange);
+                }
+            };
+
+    private final AudioManager.OnAudioFocusChangeListener mSecondaryFocusListener =
+            new AudioManager.OnAudioFocusChangeListener() {
+                @Override
+                public void onAudioFocusChange(int focusChange) {
+                    addLog(Log.INFO, "Secondary focus change:" + focusChange);
+                }
+            };
+
+    private final RadioTuner.Callback mRadioCallback = new RadioTuner.Callback() {
+        @Override
+        public void onError(int status) {
+            addLog(Log.WARN, "Radio tuner error " + status);
+        }
+
+        @Override
+        public void onConfigurationChanged(RadioManager.BandConfig config) {
+            addLog(Log.INFO, "Radio tuner configuration changed. config:" + config);
+        }
+
+        @Override
+        public void onMetadataChanged(RadioMetadata metadata) {
+            addLog(Log.INFO, "Radio tuner metadata changed. metadata:" + metadata);
+            if (metadata == null) {
+                resetMessages();
+                updateMessages();
+                return;
+            }
+            mArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST);
+            mSong = metadata.getString(RadioMetadata.METADATA_KEY_TITLE);
+            mStation = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PI);
+            updateMessages();
+        }
+
+        @Override
+        public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
+            addLog(Log.INFO, "Radio tuner program info. info:" + info);
+            mChannel = String.valueOf(info.getChannel());
+            onMetadataChanged(info.getMetadata());
+            updateMessages();
+        }
+
+    };
+    private final LinkedList<String> mLogMessages = new LinkedList<>();
+
+    private Button mOpenRadio;
+    private Button mCloseRadio;
+    private Button mGetFocus;
+    private Button mReleaseFocus;
+    private Button mRadioNext;
+    private Button mRadioPrev;
+    private Button mRadioScanCancel;
+    private ToggleButton mRadioBand;
+    private TextView mStationInfo;
+    private TextView mChannelInfo;
+    private TextView mSongInfo;
+    private TextView mArtistInfo;
+    private TextView mLog;
+
+    private Car mCar;
+    private CarAudioManager mCarAudioManager;
+    private AudioAttributes mRadioAudioAttrib;
+    private AudioManager mAudioManager;
+    private boolean hasSecondaryFocus;
+    private boolean isScanning;
+    private RadioTuner mRadioTuner;
+    private RadioManager mRadioManager;
+    private RadioManager.FmBandDescriptor mFmDescriptor;
+    private RadioManager.AmBandDescriptor mAmDescriptor;
+    private String mStation;
+    private String mChannel;
+    private String mSong;
+    private String mArtist;
+    private String mNaString;
+
+    private RadioManager.BandConfig mFmConfig;
+    private RadioManager.BandConfig mAmConfig;
+
+    private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        if (DBG) {
+            Log.i(TAG, "onCreateView");
+        }
+
+        init();
+        View view = inflater.inflate(R.layout.radio, container, false);
+
+        mOpenRadio = (Button) view.findViewById(R.id.button_open_radio);
+        mCloseRadio = (Button) view.findViewById(R.id.button_close_radio);
+        mGetFocus = (Button) view.findViewById(R.id.button_get_focus_in_radio);
+        mReleaseFocus = (Button) view.findViewById(R.id.button_release_focus_in_radio);
+        mRadioNext = (Button) view.findViewById(R.id.button_radio_next);
+        mRadioPrev = (Button) view.findViewById(R.id.button_radio_prev);
+        mRadioScanCancel = (Button) view.findViewById(R.id.button_radio_scan_cancel);
+        mRadioBand = (ToggleButton) view.findViewById(R.id.button_band_selection);
+
+        mStationInfo = (TextView) view.findViewById(R.id.radio_station_info);
+        mChannelInfo = (TextView) view.findViewById(R.id.radio_channel_info);
+        mSongInfo = (TextView) view.findViewById(R.id.radio_song_info);
+        mArtistInfo = (TextView) view.findViewById(R.id.radio_artist_info);
+
+        mLog = (TextView) view.findViewById(R.id.radio_log);
+        mLog.setMovementMethod(new ScrollingMovementMethod());
+
+        mNaString = getContext().getString(R.string.radio_na);
+
+        addHandlers();
+        updateStates();
+
+        return view;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        resetMessages();
+        updateStates();
+        updateMessages();
+        resetLog();
+    }
+
+    private void init() {
+        mCar = Car.createCar(getContext(), new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+                mRadioAudioAttrib = mCarAudioManager.getAudioAttributesForCarUsage(
+                        CarAudioManager.CAR_AUDIO_USAGE_RADIO);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+            }
+        }, Looper.getMainLooper());
+        mCar.connect();
+        mAudioManager = (AudioManager) getContext().getSystemService(
+                Context.AUDIO_SERVICE);
+        initializeRadio();
+    }
+
+    private void initializeRadio() {
+        mRadioManager = (RadioManager) getContext().getSystemService(Context.RADIO_SERVICE);
+
+        if (mRadioManager == null) {
+            throw new IllegalStateException("RadioManager could not be loaded.");
+        }
+
+        int status = mRadioManager.listModules(mModules);
+        if (status != RadioManager.STATUS_OK) {
+            throw new IllegalStateException("Load modules failed with status: " + status);
+        }
+
+        if (mModules.size() == 0) {
+            throw new IllegalStateException("No radio modules on device.");
+        }
+
+        boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
+
+        // Load the possible radio bands. For now, just accept FM and AM bands.
+        for (RadioManager.BandDescriptor band : mModules.get(0).getBands()) {
+            if (isDebugLoggable) {
+                Log.d(TAG, "loading band: " + band.toString());
+            }
+
+            if (mFmDescriptor == null && band.getType() == RadioManager.BAND_FM) {
+                mFmDescriptor = (RadioManager.FmBandDescriptor) band;
+            }
+
+            if (mAmDescriptor == null && band.getType() == RadioManager.BAND_AM) {
+                mAmDescriptor = (RadioManager.AmBandDescriptor) band;
+            }
+        }
+
+        if (mFmDescriptor == null && mAmDescriptor == null) {
+            throw new IllegalStateException("No AM and FM radio bands could be loaded.");
+        }
+
+        mFmConfig = new RadioManager.FmBandConfig.Builder(mFmDescriptor)
+                .setStereo(true)
+                .build();
+        mAmConfig = new RadioManager.AmBandConfig.Builder(mAmDescriptor)
+                .setStereo(true)
+                .build();
+    }
+
+    private void addHandlers() {
+        mOpenRadio.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                handleRadioStart();
+                updateStates();
+            }
+        });
+        mCloseRadio.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                handleRadioEnd();
+                updateStates();
+            }
+        });
+        mGetFocus.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Get secondary focus");
+                }
+                mAudioManager.requestAudioFocus(mSecondaryFocusListener,
+                        AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+                hasSecondaryFocus = true;
+                updateStates();
+            }
+        });
+        mReleaseFocus.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Release secondary focus");
+                }
+                mAudioManager.abandonAudioFocus(mSecondaryFocusListener);
+                hasSecondaryFocus = false;
+                updateStates();
+            }
+        });
+        mRadioNext.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Next radio station");
+                }
+                if (mRadioTuner != null) {
+                    mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
+                }
+                updateStates();
+            }
+        });
+        mRadioPrev.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Previous radio station");
+                }
+                if (mRadioTuner != null) {
+                    mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
+                }
+                updateStates();
+            }
+        });
+        mRadioScanCancel.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Cancel radio scan");
+                }
+                if (mRadioTuner != null) {
+                    mRadioTuner.cancel();
+                }
+                updateStates();
+            }
+        });
+        mRadioBand.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                if (DBG) {
+                    Log.i(TAG, "Changing ratio band");
+                }
+                if (mRadioTuner != null) {
+                    mRadioTuner.close();
+                    mRadioTuner = null;
+                    mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(),
+                            mRadioBand.isChecked() ? mFmConfig : mAmConfig, true, mRadioCallback , null);
+                }
+                resetMessages();
+                updateMessages();
+                updateStates();
+            }
+        });
+    }
+
+    private void updateStates() {
+        mOpenRadio.setEnabled(mRadioTuner == null);
+        mCloseRadio.setEnabled(mRadioTuner != null);
+        mGetFocus.setEnabled(!hasSecondaryFocus);
+        mReleaseFocus.setEnabled(hasSecondaryFocus);
+        mRadioNext.setEnabled(mRadioTuner != null);
+        mRadioPrev.setEnabled(mRadioTuner != null);
+        mRadioBand.setEnabled(mRadioTuner != null);
+    }
+
+    private void updateMessages() {
+        mStationInfo.setText(getContext().getString
+                (R.string.radio_station_info, mStation == null ? mNaString : mStation));
+        mChannelInfo.setText(getContext().getString
+                (R.string.radio_channel_info, mChannel == null ? mNaString : mChannel));
+        mArtistInfo.setText(getContext().getString
+                (R.string.radio_artist_info, mArtist == null ? mNaString : mArtist));
+        mSongInfo.setText(getContext().getString
+                (R.string.radio_song_info, mSong == null ? mNaString : mSong));
+    }
+
+    private void resetMessages() {
+        mStation = null;
+        mChannel = null;
+        mSong = null;
+        mArtist = null;
+    }
+
+    private void handleRadioStart() {
+        if (mCarAudioManager == null) {
+            return;
+        }
+        if (DBG) {
+            Log.i(TAG, "Radio start");
+        }
+        mCarAudioManager.requestAudioFocus(mRadioFocusListener, mRadioAudioAttrib,
+                AudioManager.AUDIOFOCUS_GAIN, 0);
+        if (mRadioTuner != null) {
+            mRadioTuner.close();
+            mRadioTuner = null;
+        }
+        mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), mRadioBand.isChecked() ? mFmConfig : mAmConfig,
+                true, mRadioCallback /* callback */, null /* handler */);
+    }
+
+    private void handleRadioEnd() {
+        if (mCarAudioManager == null) {
+            return;
+        }
+        if (DBG) {
+            Log.i(TAG, "Radio end");
+        }
+        mCarAudioManager.abandonAudioFocus(mRadioFocusListener, mRadioAudioAttrib);
+        mRadioTuner.close();
+        mRadioTuner = null;
+    }
+
+    private void resetLog() {
+        synchronized (this) {
+            mLogMessages.clear();
+        }
+    }
+
+    private void addLog(int priority, String message) {
+        Log.println(priority, TAG, message);
+        synchronized (this) {
+            mLogMessages.add(message);
+            if (mLogMessages.size() > MAX_LOG_MESSAGES) {
+                mLogMessages.poll();
+            }
+            mLog.setText(TextUtils.join("\n", mLogMessages));
+        }
+    }
+}
+
diff --git a/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java b/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
index 72e97e0..371a9a5 100644
--- a/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
+++ b/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
@@ -117,6 +117,7 @@
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG |
+                VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG |
@@ -138,6 +139,7 @@
         assertEquals(
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG |
+                VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG |
                 VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG,
                 v.getInt32Values(
                         VehicleAudioRoutingPolicyIndex.VEHICLE_AUDIO_ROUTING_POLICY_INDEX_CONTEXTS)
diff --git a/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java b/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
index 180b3ef..29a020e 100644
--- a/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
@@ -361,8 +361,7 @@
         assertEquals(0, request[1]);
         assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
                 request[2]);
-        // no android side context for radio
-        assertEquals(0, request[3]);
+        assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
         mAudioFocusPropertyHandler.sendAudioFocusState(
                 VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
                 0,
@@ -382,7 +381,8 @@
         assertEquals(0x1 << VehicleAudioStream.VEHICLE_AUDIO_STREAM1, request[1]);
         assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
                 request[2]);
-        assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG, request[3]);
+        assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG |
+                VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
         mAudioFocusPropertyHandler.sendAudioFocusState(
                 VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
                 0x1 << VehicleAudioStream.VEHICLE_AUDIO_STREAM1,
@@ -396,7 +396,7 @@
         assertEquals(0, request[1]);
         assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
                 request[2]);
-        assertEquals(0, request[3]);
+        assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
         mAudioFocusPropertyHandler.sendAudioFocusState(
                 VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
                 0,
diff --git a/tests/vehicle_hal_test/Android.mk b/tests/vehicle_hal_test/Android.mk
new file mode 100644
index 0000000..77bc6f8
--- /dev/null
+++ b/tests/vehicle_hal_test/Android.mk
@@ -0,0 +1,39 @@
+# 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.
+#
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := AndroidVehicleHalTests
+
+# for system|priviledged permission.
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := tests
+
+# When built explicitly put it in the data partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_JAVA_LIBRARIES := libvehiclenetwork-java
+
+LOCAL_JAVA_LIBRARIES := android.car android.test.runner
+
+include $(BUILD_PACKAGE)
diff --git a/tests/vehicle_hal_test/AndroidManifest.xml b/tests/vehicle_hal_test/AndroidManifest.xml
new file mode 100644
index 0000000..5fbb62d
--- /dev/null
+++ b/tests/vehicle_hal_test/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        package="com.android.car.vehiclenetwork.haltest"
+        android:sharedUserId="android.uid.system" >
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.car.vehiclenetwork.haltest"
+            android:label="Tests for vehicle hal using vehicle network service"/>
+
+    <application android:label="VehicleHalTest">
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".TestCarProxyActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java
new file mode 100644
index 0000000..6111746
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.vehiclenetwork.haltest;
+
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_AC_ON;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_DEFROSTER;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_FAN_DIRECTION;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_FAN_SPEED;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_MAX_AC_ON;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_RECIRC_ON;
+
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHvacFanDirection;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehiclePropAccess;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
+
+/**
+ * Test HVAC vehicle HAL using vehicle network service.
+ */
+public class HvacVnsHalTest extends VnsHalBaseTestCase {
+
+    public void testAcProperty() throws Exception {
+        int propertyId = VEHICLE_PROPERTY_HVAC_AC_ON;
+        if (!isPropertyAvailable(propertyId)) {
+            return;
+        }
+
+        VehiclePropConfig config = mConfigsMap.get(propertyId);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        int zone = getFirstZoneForProperty(propertyId);
+        setPropertyAndVerify(propertyId, zone, false);
+
+        mVehicleNetwork.subscribe(propertyId, 0, 0);
+        mListener.reset();
+        mListener.addExpectedValues(propertyId, zone, true);
+        setPropertyAndVerify(propertyId, zone, true);
+        mListener.waitAndVerifyValues();
+    }
+
+    public void testRecirculateProperty() {
+        int propertyId = VEHICLE_PROPERTY_HVAC_RECIRC_ON;
+        if (!isPropertyAvailable(propertyId)) {
+            return;
+        }
+        VehiclePropConfig config = mConfigsMap.get(propertyId);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        // Verify subsequent calls ends-up with right value.
+        mVehicleNetwork.setBooleanProperty(propertyId, false);
+        mVehicleNetwork.setBooleanProperty(propertyId, true);
+        mVehicleNetwork.setBooleanProperty(propertyId, false);
+        mVehicleNetwork.setBooleanProperty(propertyId, true);
+        verifyValue(propertyId, true);
+
+        // Verify subsequent calls ends-up with right value.
+        mVehicleNetwork.setBooleanProperty(propertyId, false);
+        mVehicleNetwork.setBooleanProperty(propertyId, true);
+        mVehicleNetwork.setBooleanProperty(propertyId, false);
+        verifyValue(propertyId, false);
+
+        setPropertyAndVerify(propertyId, false);
+        setPropertyAndVerify(propertyId, true);
+        setPropertyAndVerify(propertyId, false);
+    }
+
+    public void testMaxAcProperty() throws Exception {
+        if (!isPropertyAvailable(
+                VEHICLE_PROPERTY_HVAC_MAX_AC_ON,
+                VEHICLE_PROPERTY_HVAC_AC_ON,
+                VEHICLE_PROPERTY_HVAC_RECIRC_ON)) {
+            return;
+        }
+        VehiclePropConfig config = mConfigsMap.get(VEHICLE_PROPERTY_HVAC_MAX_AC_ON);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        int acZone = getFirstZoneForProperty(VEHICLE_PROPERTY_HVAC_AC_ON);
+
+        // Turn off related properties.
+        setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, false);
+        setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, false);
+        setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_RECIRC_ON, false);
+
+        // Now turning max A/C and verify that other related HVAC props turned on.
+        mVehicleNetwork.subscribe(VEHICLE_PROPERTY_HVAC_AC_ON, 0f, acZone);
+        mVehicleNetwork.subscribe(VEHICLE_PROPERTY_HVAC_RECIRC_ON, 0f);
+        mListener.reset();
+        mListener.addExpectedValues(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+        mListener.addExpectedValues(VEHICLE_PROPERTY_HVAC_RECIRC_ON, 0, true);
+        setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, true);
+        verifyValue(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+        verifyValue(VEHICLE_PROPERTY_HVAC_RECIRC_ON, true);
+        mListener.waitAndVerifyValues();
+
+        // When max A/C turned off, A/C should remain to be turned on, but circulation should has
+        // the value it had before turning max A/C on, which if OFF in this case.
+        setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, false);
+        verifyValue(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+        verifyValue(VEHICLE_PROPERTY_HVAC_RECIRC_ON, false);
+    }
+
+    public void testDefroster() {
+        final int propertyId = VEHICLE_PROPERTY_HVAC_DEFROSTER;
+        if (!isPropertyAvailable(propertyId)) {
+            return;
+        }
+
+        VehiclePropConfig config = mConfigsMap.get(propertyId);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+            Log.i(TAG, "testDefroster, " + message);
+            setPropertyAndVerify(propertyId, zone, false);
+            setPropertyAndVerify(propertyId, zone, true);
+        });
+    }
+
+    public void testFanSpeed() throws Exception {
+        if (!isPropertyAvailable(VEHICLE_PROPERTY_HVAC_FAN_SPEED)) {
+            return;
+        }
+
+        VehiclePropConfig config = mConfigsMap.get(VEHICLE_PROPERTY_HVAC_FAN_SPEED);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        verifyIntZonedProperty(VEHICLE_PROPERTY_HVAC_FAN_SPEED);
+    }
+
+    public void testFanDirection() throws Exception {
+        final int propertyId = VEHICLE_PROPERTY_HVAC_FAN_DIRECTION;
+
+        if (!isPropertyAvailable(propertyId)) {
+            return;
+        }
+
+        VehiclePropConfig config = mConfigsMap.get(propertyId);
+        assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32, config.getValueType());
+        assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+        // Assert setting edge-case values.
+        iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+            setPropertyAndVerify(message, propertyId, zone, minValue);
+            setPropertyAndVerify(message, propertyId, zone, maxValue);
+        });
+
+        iterateOnZones(config, ((message, zone, minValue, maxValue)
+                -> setPropertyAndVerify(message, propertyId, zone,
+                        VehicleHvacFanDirection.VEHICLE_HVAC_FAN_DIRECTION_DEFROST_AND_FLOOR)));
+    }
+}
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java
new file mode 100644
index 0000000..c10dbd0
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java
@@ -0,0 +1,210 @@
+/*
+ * 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.vehiclenetwork.haltest;
+
+import static java.lang.Integer.toHexString;
+import static junit.framework.Assert.assertTrue;
+
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetwork.VehicleNetworkListener;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValues;
+import com.android.car.vehiclenetwork.VehicleNetworkProtoUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class must be used in testing environment only. Here's an example of usage:
+ * <ul>
+ *     <li>listener.reset();
+ *     <li>listener.addExpectedValues(myPropertyId, myZone, 10, 20, 30)
+ *     <li>... set values through VehicleNetworkService ...
+ *     <li>listener.waitAndVerifyValues()
+ *</ul>
+ *
+ */
+class RecordingVehicleNetworkListener implements VehicleNetworkListener {
+
+    private final static String TAG = VnsHalBaseTestCase.TAG;
+    private final static int EVENTS_WAIT_TIMEOUT_MS = 2000;
+
+    private final Set<NormalizedValue> mExpectedValues = new HashSet<>();
+    // Using Set here instead of List as we probably shouldn't assert the order event was received.
+    private final Set<NormalizedValue> mRecordedEvents = new HashSet<>();
+
+    synchronized void reset() {
+        mExpectedValues.clear();
+        mRecordedEvents.clear();
+    }
+
+    void addExpectedValues(int propertyId, int zone, Object... values) {
+        for (Object value : values) {
+            mExpectedValues.add(NormalizedValue.createFor(propertyId, zone, value));
+        }
+    }
+
+    /**
+     * Waits for events to come for #EVENTS_WAIT_TIMEOUT_MS milliseconds and asserts that recorded
+     * values match with expected.
+     * */
+    synchronized void waitAndVerifyValues() throws InterruptedException {
+        long currentTime = System.currentTimeMillis();
+        long deadline = currentTime + EVENTS_WAIT_TIMEOUT_MS;
+        while (currentTime < deadline && !isExpectedMatchedRecorded()) {
+            wait(deadline - currentTime);
+            currentTime = System.currentTimeMillis();
+        }
+        assertTrue("Expected values: " + Arrays.toString(mExpectedValues.toArray())
+                        + " doesn't match recorded: " + Arrays.toString(mRecordedEvents.toArray()),
+                isExpectedMatchedRecorded());
+    }
+
+    private boolean isExpectedMatchedRecorded() {
+        for (NormalizedValue expectedValue : mExpectedValues) {
+            if (!mRecordedEvents.contains(expectedValue)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void onVehicleNetworkEvents(VehiclePropValues values) {
+        for (VehiclePropValue value : values.getValuesList()) {
+            Log.d(TAG, "onVehicleNetworkEvents, value: "
+                    + VehicleNetworkProtoUtil.VehiclePropValueToString(value));
+
+            synchronized (this) {
+                mRecordedEvents.add(NormalizedValue.createFor(value));
+                notifyAll();
+            }
+        }
+    }
+
+    @Override
+    public void onHalError(int errorCode, int property, int operation) {
+        // TODO Auto-generated method stub
+    }
+
+    @Override
+    public void onHalRestart(boolean inMocking) {
+        // TODO Auto-generated method stub
+    }
+
+    // To be used to compare expected vs recorded values.
+    private static class NormalizedValue {
+        private final int propertyId;
+        private final int zone;
+        private final List<Object> value;
+
+        static NormalizedValue createFor(VehiclePropValue value) {
+            return new NormalizedValue(value.getProp(), value.getZone(), getObjectValue(value));
+        }
+
+        static NormalizedValue createFor(int propertyId, int zone, Object value) {
+            return new NormalizedValue(propertyId, zone, wrapSingleObjectToList(value));
+        }
+
+        // Do not call this ctor directly, use appropriate factory methods to create an object.
+        private NormalizedValue(int propertyId, int zone, List<Object> value) {
+            this.propertyId = propertyId;
+            this.zone = zone;
+            this.value = value;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            NormalizedValue propValue = (NormalizedValue) o;
+            return propertyId == propValue.propertyId &&
+                    zone == propValue.zone &&
+                    Objects.equals(value, propValue.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(propertyId, zone, value);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + " { "
+                    + "prop: 0x" + toHexString(propertyId)
+                    + ", zone: 0x" + toHexString(zone)
+                    + ", value: " + Arrays.toString(value.toArray())
+                    + " }";
+        }
+
+        private static List<Object> wrapSingleObjectToList(Object value) {
+            if (value instanceof Integer
+                    || value instanceof Float) {
+                List<Object> list = new ArrayList<>(1);
+                list.add(value);
+                return list;
+            } else if (value instanceof Boolean) {
+                List<Object> list = new ArrayList<>(1);
+                list.add((Boolean)value ? 1 : 0);
+                return list;
+            } else if (value instanceof Collection<?>) {
+                return new ArrayList<>((Collection<?>) value);
+            } else {
+                throw new IllegalArgumentException("Unexpected type: " + value);
+            }
+        }
+
+        // Converts any VehiclePropValue to either ArrayList<Integer> or ArrayList<Float>
+        private static List<Object> getObjectValue(VehiclePropValue val) {
+            switch (val.getValueType()) {
+                case VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_INT32:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC2:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC3:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC4:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC2:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC3:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC4:
+                    return new ArrayList<>(val.getInt32ValuesList());
+                case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC2:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC3:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC4:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC2:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC3:
+                case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC4:
+                    return new ArrayList<>(val.getFloatValuesList());
+                default:
+                    throw new IllegalArgumentException("Unexpected type: " + val.getValueType());
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java
new file mode 100644
index 0000000..adcc336
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java
@@ -0,0 +1,212 @@
+/*
+ * 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.vehiclenetwork.haltest;
+
+import static java.lang.Integer.toHexString;
+
+import android.car.VehicleZoneUtil;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetwork;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for testing vehicle HAL using vehicle network service (VNS).
+ */
+public class VnsHalBaseTestCase extends AndroidTestCase {
+
+    protected static final String TAG = "VnsHalTest";
+
+    protected static final int PROP_TIMEOUT_MS = 5000;
+    protected static final int NO_ZONE = -1;
+
+    private final HandlerThread mHandlerThread =
+            new HandlerThread(VnsHalBaseTestCase.class.getSimpleName());
+    protected VehicleNetwork mVehicleNetwork;
+    protected RecordingVehicleNetworkListener mListener;
+
+    protected Map<Integer, VehiclePropConfig> mConfigsMap;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHandlerThread.start();
+        mListener = new RecordingVehicleNetworkListener();
+        mVehicleNetwork = VehicleNetwork.createVehicleNetwork(mListener,
+                mHandlerThread.getLooper());
+        setupConfigsMap();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mHandlerThread.quit();
+    }
+
+    private void setupConfigsMap() {
+        mConfigsMap = new HashMap<>();
+        for (VehiclePropConfig config : mVehicleNetwork.listProperties().getConfigsList()) {
+            mConfigsMap.put(config.getProp(), config);
+        }
+        assertTrue(mConfigsMap.size() > 0);
+    }
+
+    protected boolean isPropertyAvailable(int... properties) {
+        assertTrue(properties.length > 0);
+
+        for (int propertyId : properties) {
+            if (!mConfigsMap.containsKey(propertyId)) {
+                // Property is not supported by vehicle HAL, nothing to test.
+                Log.w(TAG, "Property: 0x" + Integer.toHexString(propertyId)
+                        + " is not available, ignoring...");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected void setPropertyAndVerify(int propertyId, boolean switchOn) {
+        setPropertyAndVerify(propertyId, NO_ZONE, switchOn);
+    }
+
+    protected void setPropertyAndVerify(int propertyId, int zone, boolean switchOn) {
+        int val = switchOn ? 1 : 0;
+
+        if (zone == NO_ZONE) {
+            mVehicleNetwork.setBooleanProperty(propertyId, switchOn);
+        } else {
+            mVehicleNetwork.setZonedBooleanProperty(propertyId, zone, switchOn);
+        }
+        verifyValue(propertyId, zone, val);
+    }
+
+    protected void setPropertyAndVerify(String message, int propertyId, int zone, int value) {
+        if (zone == NO_ZONE) {
+            mVehicleNetwork.setIntProperty(propertyId, value);
+        } else {
+            mVehicleNetwork.setZonedIntProperty(propertyId, zone, value);
+        }
+        verifyValue(message, propertyId, zone, value);
+    }
+
+    protected void verifyValue(int propertyId, boolean val) {
+        verifyValue(propertyId, NO_ZONE, val);
+    }
+
+    protected void verifyValue(int propertyId, int zone, boolean val) {
+        int intVal = val ? 1 : 0;
+        assertEquals(intVal, waitForIntValue(propertyId, zone, intVal));
+    }
+
+    protected void verifyValue(int propertyId, int zone, int val) {
+        assertEquals(val, waitForIntValue(propertyId, zone, val));
+    }
+
+    protected void verifyValue(String message, int propertyId, int zone, int val) {
+        assertEquals(message, val, waitForIntValue(propertyId, zone, val));
+    }
+
+    protected int getFirstZoneForProperty(int propertyId) {
+        return VehicleZoneUtil.getFirstZone(mConfigsMap.get(propertyId).getZones());
+    }
+
+    protected void verifyIntZonedProperty(int propertyId) throws InterruptedException {
+        if (!isPropertyAvailable(propertyId)) {
+            return;
+        }
+
+        VehiclePropConfig config = mConfigsMap.get(propertyId);
+
+        // Assert setting edge-case values.
+        iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+            setPropertyAndVerify(message, propertyId, zone, minValue);
+            setPropertyAndVerify(message, propertyId, zone, maxValue);
+        });
+
+        // Setting out of the range values.
+        iterateOnZones(config, ((message, zone, minValue, maxValue) -> {
+            mVehicleNetwork.setZonedIntProperty(propertyId, zone, minValue - 1);
+            verifyValue(message, propertyId, zone, minValue);
+
+            mVehicleNetwork.setZonedIntProperty(propertyId, zone, maxValue + 20);
+            verifyValue(message, propertyId, zone, maxValue);
+        }));
+
+        // Verify that subsequent SET calls will result in correct value at the end.
+        mVehicleNetwork.subscribe(propertyId, 0f, config.getZones());
+        int zone = VehicleZoneUtil.getFirstZone(config.getZones());
+        int minValue = config.getInt32Mins(0);
+        int maxValue = config.getInt32Maxs(0);
+        int finalValue = (minValue + maxValue) / 2;
+        mListener.reset();
+        // We can only expect to see finalValue in the events as vehicle HAL may batch
+        // set commands and use only last value.
+        mListener.addExpectedValues(propertyId, zone, finalValue);
+        mVehicleNetwork.setZonedIntProperty(propertyId, zone, minValue);
+        mVehicleNetwork.setZonedIntProperty(propertyId, zone, maxValue);
+        mVehicleNetwork.setZonedIntProperty(propertyId, zone, finalValue);
+        verifyValue(propertyId, zone, finalValue);
+        mListener.waitAndVerifyValues();
+        mVehicleNetwork.unsubscribe(propertyId);
+    }
+
+    protected int waitForIntValue(int propertyId, int zone, int expectedValue) {
+        int actualValue = mVehicleNetwork.getZonedIntProperty(propertyId, zone);
+        long deadline = System.currentTimeMillis() + PROP_TIMEOUT_MS;
+
+        while (System.currentTimeMillis() <= deadline && actualValue != expectedValue) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ex) {
+                throw new RuntimeException(ex);
+            }
+            actualValue = mVehicleNetwork.getZonedIntProperty(propertyId, zone);
+        }
+
+        return actualValue;
+    }
+
+    protected void iterateOnZones(VehiclePropConfig config, IntZonesFunctor f) {
+        int[] zones = VehicleZoneUtil.listAllZones(config.getZones());
+        int zoneIndex = 0;
+        boolean isBooleanType = config.getValueType() ==
+                VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN;
+
+        for (int zone : zones) {
+            int minValue = isBooleanType ? 0 : config.getInt32Mins(zoneIndex);
+            int maxValue = isBooleanType ? 1 : config.getInt32Maxs(zoneIndex);
+            String message = "PropertyId: 0x" + toHexString(config.getProp())
+                    + ", zone[" + zoneIndex + "]: 0x" + toHexString(zone);
+            f.func(message, zone, minValue, maxValue);
+            zoneIndex++;
+        }
+
+    }
+
+    protected interface IntZonesFunctor {
+        void func(String message, int zone, int minValue, int maxValue);
+    }
+
+
+}