change some HVAC properties to be zoned
am: 4fff3d5

* commit '4fff3d5cc4112a341c331688bda9fcb65ed9016a':
  change some HVAC properties to be zoned

Change-Id: I3f88e3845668e225bd451a9f6a8fd96b1a27084c
diff --git a/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoInstrumentClusterRenderer.java b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoInstrumentClusterRenderer.java
index 71cccdb..3b09be7 100644
--- a/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoInstrumentClusterRenderer.java
+++ b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoInstrumentClusterRenderer.java
@@ -15,32 +15,61 @@
  */
 package android.car.cluster.demorenderer;
 
-import android.car.cluster.renderer.DisplayConfiguration;
+import android.app.Presentation;
 import android.car.cluster.renderer.InstrumentClusterRenderer;
 import android.car.cluster.renderer.NavigationRenderer;
-import android.car.navigation.CarNavigationInstrumentCluster;
 import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Display;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 
 /**
  * Demo implementation of {@code InstrumentClusterRenderer}.
  */
 public class DemoInstrumentClusterRenderer extends InstrumentClusterRenderer {
 
+    private final static String TAG = DemoInstrumentClusterRenderer.class.getSimpleName();
+
     private DemoInstrumentClusterView mView;
     private Context mContext;
     private CallStateMonitor mPhoneStatusMonitor;
     private MediaStateMonitor mMediaStateMonitor;
     private DemoPhoneRenderer mPhoneRenderer;
     private DemoMediaRenderer mMediaRenderer;
+    private Presentation mPresentation;
 
     @Override
     public void onCreate(Context context) {
         mContext = context;
+
+        final Display display = getInstrumentClusterDisplay(context);
+
+        if (display != null) {
+            runOnMainThread(() -> {
+                Log.d(TAG, "Initializing renderer in main thread.");
+                try {
+                    mPresentation = new InstrumentClusterPresentation(mContext, display);
+
+                    ViewGroup rootView = (ViewGroup) LayoutInflater.from(mContext).inflate(
+                            R.layout.instrument_cluster, null);
+
+                    mPresentation.setContentView(rootView);
+                    View rendererView = createView();
+                    rootView.addView(rendererView);
+                } catch (Exception e) {
+                    Log.e(TAG, e.getMessage(), e);
+                    throw e;
+                }
+            });
+        }
     }
 
-    @Override
-    public View onCreateView(DisplayConfiguration displayConfiguration) {
+    private View createView() {
         mView = new DemoInstrumentClusterView(mContext);
         mPhoneRenderer = new DemoPhoneRenderer(mView);
         mMediaRenderer = new DemoMediaRenderer(mView);
@@ -49,33 +78,55 @@
 
     @Override
     public void onStart() {
-        mPhoneStatusMonitor = new CallStateMonitor(mContext, mPhoneRenderer);
-        mMediaStateMonitor = new MediaStateMonitor(mContext, mMediaRenderer);
+        runOnMainThread(() -> {
+            Log.d(TAG, "onStart");
+            mPhoneStatusMonitor = new CallStateMonitor(mContext, mPhoneRenderer);
+            mMediaStateMonitor = new MediaStateMonitor(mContext, mMediaRenderer);
+            mPresentation.show();
+        });
     }
 
     @Override
     public void onStop() {
-        if (mPhoneStatusMonitor != null) {
-            mPhoneStatusMonitor.release();
-            mPhoneStatusMonitor = null;
-        }
+        runOnMainThread(() -> {
+            if (mPhoneStatusMonitor != null) {
+                mPhoneStatusMonitor.release();
+                mPhoneStatusMonitor = null;
+            }
 
-        if (mMediaStateMonitor != null) {
-            mMediaStateMonitor.release();
-            mMediaStateMonitor = null;
-        }
-        mPhoneRenderer = null;
-        mMediaRenderer = null;
+            if (mMediaStateMonitor != null) {
+                mMediaStateMonitor.release();
+                mMediaStateMonitor = null;
+            }
+            mPhoneRenderer = null;
+            mMediaRenderer = null;
+        });
     }
 
     @Override
     protected NavigationRenderer createNavigationRenderer() {
-        return new DemoNavigationRenderer(mView);
+        return ThreadSafeNavigationRenderer.createFor(
+                Looper.getMainLooper(),
+                new DemoNavigationRenderer(mView));
     }
 
-    @Override
-    public CarNavigationInstrumentCluster getNavigationProperties() {
-        // TODO
+    private static Display getInstrumentClusterDisplay(Context context) {
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display[] displays = displayManager.getDisplays();
+
+        Log.d(TAG, "There are currently " + displays.length + " displays connected.");
+        for (Display display : displays) {
+            Log.d(TAG, "  " + display);
+        }
+
+        if (displays.length > 1) {
+            // TODO: assuming that secondary display is instrument cluster. Put this into settings?
+            return displays[1];
+        }
         return null;
     }
+
+    private static void runOnMainThread(Runnable runnable) {
+        new Handler(Looper.getMainLooper()).post(runnable);
+    }
 }
diff --git a/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoNavigationRenderer.java b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoNavigationRenderer.java
index 5ddb822..5588546 100644
--- a/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoNavigationRenderer.java
+++ b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/DemoNavigationRenderer.java
@@ -20,6 +20,7 @@
 import static android.car.navigation.CarNavigationManager.TURN_TURN;
 
 import android.car.cluster.renderer.NavigationRenderer;
+import android.car.navigation.CarNavigationInstrumentCluster;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.util.Log;
@@ -53,6 +54,12 @@
     }
 
     @Override
+    public CarNavigationInstrumentCluster getNavigationProperties() {
+        // TODO
+        return null;
+    }
+
+    @Override
     public void onStartNavigation() {
         mView.showNavigation();
     }
diff --git a/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/InstrumentClusterPresentation.java b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/InstrumentClusterPresentation.java
new file mode 100644
index 0000000..1547a24
--- /dev/null
+++ b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/InstrumentClusterPresentation.java
@@ -0,0 +1,31 @@
+/*
+ * 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.car.cluster.demorenderer;
+
+import android.app.Presentation;
+import android.content.Context;
+import android.view.Display;
+import android.view.WindowManager;
+
+/**
+ * Presentation class.
+ */
+public class InstrumentClusterPresentation extends Presentation {
+    public InstrumentClusterPresentation(Context outerContext, Display display) {
+        super(outerContext, display);
+        getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+    }
+}
diff --git a/service/src/com/android/car/cluster/renderer/RendererHandler.java b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/RendererHandler.java
similarity index 96%
rename from service/src/com/android/car/cluster/renderer/RendererHandler.java
rename to car-cluster-demo-renderer/src/android/car/cluster/demorenderer/RendererHandler.java
index b999dde..6989a9c 100644
--- a/service/src/com/android/car/cluster/renderer/RendererHandler.java
+++ b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/RendererHandler.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.car.cluster.renderer;
+package android.car.cluster.demorenderer;
 
 import android.os.Handler;
 import android.os.Looper;
diff --git a/service/src/com/android/car/cluster/renderer/ThreadSafeNavigationRenderer.java b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/ThreadSafeNavigationRenderer.java
similarity index 76%
rename from service/src/com/android/car/cluster/renderer/ThreadSafeNavigationRenderer.java
rename to car-cluster-demo-renderer/src/android/car/cluster/demorenderer/ThreadSafeNavigationRenderer.java
index 33dde7a..e9327d8 100644
--- a/service/src/com/android/car/cluster/renderer/ThreadSafeNavigationRenderer.java
+++ b/car-cluster-demo-renderer/src/android/car/cluster/demorenderer/ThreadSafeNavigationRenderer.java
@@ -13,15 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.car.cluster.renderer;
+package android.car.cluster.demorenderer;
 
 import android.annotation.Nullable;
 import android.car.cluster.renderer.NavigationRenderer;
+import android.car.navigation.CarNavigationInstrumentCluster;
 import android.graphics.Bitmap;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 
+import java.util.concurrent.CountDownLatch;
+
 /**
  * A wrapper over {@link NavigationRenderer} that runs all its methods in the context of provided
  * looper. It is guaranteed that all calls will be invoked in order they were called.
@@ -29,6 +32,7 @@
 public class ThreadSafeNavigationRenderer extends NavigationRenderer {
 
     private final Handler mHandler;
+    private final NavigationRenderer mRenderer;
 
     private final static int MSG_NAV_START = 1;
     private final static int MSG_NAV_STOP = 2;
@@ -42,10 +46,21 @@
     }
 
     private ThreadSafeNavigationRenderer(Looper looper, NavigationRenderer renderer) {
+        mRenderer = renderer;
         mHandler = new NavigationRendererHandler(looper, renderer);
     }
 
     @Override
+    public CarNavigationInstrumentCluster getNavigationProperties() {
+        return runAndWaitResult(mHandler, new RunnableWithResult<CarNavigationInstrumentCluster>() {
+            @Override
+            protected CarNavigationInstrumentCluster createResult() {
+                return mRenderer.getNavigationProperties();
+            }
+        });
+    }
+
+    @Override
     public void onStartNavigation() {
         mHandler.sendMessage(mHandler.obtainMessage(MSG_NAV_START));
     }
@@ -98,6 +113,20 @@
         }
     }
 
+    private static <E> E runAndWaitResult(Handler handler, RunnableWithResult<E> runnable) {
+        CountDownLatch latch = new CountDownLatch(1);
+        handler.post(() -> {
+            runnable.run();
+            latch.countDown();
+        });
+        try {
+            latch.wait();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return runnable.getResult();
+    }
+
     private static class NextTurn {
         private final int event;
         private final String road;
@@ -116,4 +145,19 @@
             this.turnSide = turnSide;
         }
     }
+
+    public abstract class RunnableWithResult<T> implements Runnable {
+        private volatile T result;
+
+        protected abstract T createResult();
+
+        @Override
+        public void run() {
+            result = createResult();
+        }
+
+        public T getResult() {
+            return result;
+        }
+    }
 }
diff --git a/car-cluster-logging-renderer/Android.mk b/car-cluster-logging-renderer/Android.mk
new file mode 100644
index 0000000..8fb7674
--- /dev/null
+++ b/car-cluster-logging-renderer/Android.mk
@@ -0,0 +1,34 @@
+# 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.
+#
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := android.car.cluster.loggingrenderer
+
+# Each update should be signed by OEMs
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_JAVA_LIBRARIES += android.car
+
+include $(BUILD_PACKAGE)
diff --git a/car-cluster-logging-renderer/AndroidManifest.xml b/car-cluster-logging-renderer/AndroidManifest.xml
new file mode 100644
index 0000000..20ee7d1
--- /dev/null
+++ b/car-cluster-logging-renderer/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?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"
+      package="android.car.cluster.loggingrenderer"
+      android:versionCode="1"
+      android:versionName="1.0">
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"/>
+</manifest>
diff --git a/car-cluster-logging-renderer/proguard.flags b/car-cluster-logging-renderer/proguard.flags
new file mode 100644
index 0000000..22cc22d
--- /dev/null
+++ b/car-cluster-logging-renderer/proguard.flags
@@ -0,0 +1,3 @@
+-verbose
+-keep @com.android.internal.annotations.VisibleForTesting class *
+
diff --git a/car-cluster-logging-renderer/res/drawable-hdpi/ic_launcher.png b/car-cluster-logging-renderer/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/car-cluster-logging-renderer/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/car-cluster-logging-renderer/res/drawable-ldpi/ic_launcher.png b/car-cluster-logging-renderer/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/car-cluster-logging-renderer/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/car-cluster-logging-renderer/res/drawable-mdpi/ic_launcher.png b/car-cluster-logging-renderer/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/car-cluster-logging-renderer/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/car-cluster-logging-renderer/res/drawable-xhdpi/ic_launcher.png b/car-cluster-logging-renderer/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/car-cluster-logging-renderer/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/car-cluster-logging-renderer/res/values/strings.xml b/car-cluster-logging-renderer/res/values/strings.xml
new file mode 100644
index 0000000..f36db0d
--- /dev/null
+++ b/car-cluster-logging-renderer/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<resources>
+    <string name="app_name">LOGGING_INSTRUMENT_CLUSTER_RENDERER</string>
+</resources>
diff --git a/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/InstrumentClusterRendererFactory.java b/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/InstrumentClusterRendererFactory.java
new file mode 100644
index 0000000..d73ede4
--- /dev/null
+++ b/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/InstrumentClusterRendererFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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.car.cluster.loggingrenderer;
+
+import android.car.cluster.renderer.InstrumentClusterRenderer;
+
+/**
+ * This factory class will be requested by android.car using reflection in order to get an instance
+ * of {@link InstrumentClusterRenderer}.
+ */
+@SuppressWarnings("unused")  /* Used by reflection. */
+public class InstrumentClusterRendererFactory {
+    public static InstrumentClusterRenderer createRenderer() {
+        return new LoggingInstrumentClusterRenderer();
+    }
+}
diff --git a/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/LoggingInstrumentClusterRenderer.java b/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/LoggingInstrumentClusterRenderer.java
new file mode 100644
index 0000000..1875792
--- /dev/null
+++ b/car-cluster-logging-renderer/src/android/car/cluster/loggingrenderer/LoggingInstrumentClusterRenderer.java
@@ -0,0 +1,88 @@
+/*
+ * 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.car.cluster.loggingrenderer;
+
+import android.car.cluster.renderer.InstrumentClusterRenderer;
+import android.car.cluster.renderer.NavigationRenderer;
+import android.car.navigation.CarNavigationInstrumentCluster;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+/**
+ * Dummy implementation of {@code InstrumentClusterRenderer} that just traces all interaction.
+ */
+public class LoggingInstrumentClusterRenderer extends InstrumentClusterRenderer {
+
+    private final static String TAG = LoggingInstrumentClusterRenderer.class.getSimpleName();
+
+    @Override
+    public void onCreate(Context context) {
+        Log.i(TAG, "onCreate, context: " + context);
+    }
+
+    @Override
+    public void onStart() {
+        Log.i(TAG, "onStart");
+    }
+
+    @Override
+    public void onStop() {
+        Log.i(TAG, "onStop");
+    }
+
+    @Override
+    protected NavigationRenderer createNavigationRenderer() {
+        NavigationRenderer navigationRenderer = new NavigationRenderer() {
+            @Override
+            public CarNavigationInstrumentCluster getNavigationProperties() {
+                Log.i(TAG, "getNavigationProperties");
+                CarNavigationInstrumentCluster config =
+                        CarNavigationInstrumentCluster.createCluster(1000);
+                Log.i(TAG, "getNavigationProperties, returns: " + config);
+                return config;
+            }
+
+
+            @Override
+            public void onStartNavigation() {
+                Log.i(TAG, "onStartNavigation");
+            }
+
+            @Override
+            public void onStopNavigation() {
+                Log.i(TAG, "onStopNavigation");
+            }
+
+            @Override
+            public void onNextTurnChanged(int event, String road, int turnAngle, int turnNumber,
+                    Bitmap image, int turnSide) {
+                Log.i(TAG, "event: " + event + ", road: " + road + ", turnAngle: " + turnAngle
+                        + ", turnNumber: " + turnNumber + ", image: " + image + ", turnSide: "
+                        + turnSide);
+            }
+
+            @Override
+            public void onNextTurnDistanceChanged(int distanceMeters, int timeSeconds) {
+                Log.i(TAG, "onNextTurnDistanceChanged, distanceMeters: " + distanceMeters
+                        + ", timeSeconds: " + timeSeconds);
+            }
+        };
+
+        Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer);
+        return navigationRenderer;
+    }
+}
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index a24febf..56705c7 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -14,6 +14,7 @@
     field public static final int CONNECTION_TYPE_EMBEDDED = 5; // 0x5
     field public static final java.lang.String INFO_SERVICE = "info";
     field public static final java.lang.String PACKAGE_SERVICE = "package";
+    field public static final java.lang.String PERMISSION_CAR_CONTROL_AUDIO_VOLUME = "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
     field public static final java.lang.String PERMISSION_FUEL = "android.car.permission.CAR_FUEL";
     field public static final java.lang.String PERMISSION_MILEAGE = "android.car.permission.CAR_MILEAGE";
     field public static final java.lang.String PERMISSION_SPEED = "android.car.permission.CAR_SPEED";
@@ -329,8 +330,12 @@
   public class CarAudioManager {
     method public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method public android.media.AudioAttributes getAudioAttributesForCarUsage(int);
+    method public int getStreamMaxVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamMinVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamVolume(int) throws android.car.CarNotConnectedException;
     method public void onCarDisconnected();
     method public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException;
+    method public void setStreamVolume(int, int, int) throws android.car.CarNotConnectedException;
     field public static final int CAR_AUDIO_USAGE_ALARM = 6; // 0x6
     field public static final int CAR_AUDIO_USAGE_DEFAULT = 0; // 0x0
     field public static final int CAR_AUDIO_USAGE_MUSIC = 1; // 0x1
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 18239c4..26d439c 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -17,6 +17,7 @@
     field public static final java.lang.String INFO_SERVICE = "info";
     field public static final java.lang.String PACKAGE_SERVICE = "package";
     field public static final java.lang.String PERMISSION_CAR_CAMERA = "android.car.permission.CAR_CAMERA";
+    field public static final java.lang.String PERMISSION_CAR_CONTROL_AUDIO_VOLUME = "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
     field public static final java.lang.String PERMISSION_CAR_HVAC = "android.car.permission.CAR_HVAC";
     field public static final java.lang.String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
     field public static final java.lang.String PERMISSION_CAR_RADIO = "android.car.permission.CAR_RADIO";
@@ -301,17 +302,16 @@
   public abstract class InstrumentClusterRenderer {
     ctor public InstrumentClusterRenderer();
     method protected abstract android.car.cluster.renderer.NavigationRenderer createNavigationRenderer();
-    method public abstract android.car.navigation.CarNavigationInstrumentCluster getNavigationProperties();
     method public synchronized android.car.cluster.renderer.NavigationRenderer getNavigationRenderer();
     method public final synchronized void initialize();
     method public abstract void onCreate(android.content.Context);
-    method public abstract android.view.View onCreateView(android.car.cluster.renderer.DisplayConfiguration);
     method public abstract void onStart();
     method public abstract void onStop();
   }
 
   public abstract class NavigationRenderer {
     ctor public NavigationRenderer();
+    method public abstract android.car.navigation.CarNavigationInstrumentCluster getNavigationProperties();
     method public abstract void onNextTurnChanged(int, java.lang.String, int, int, android.graphics.Bitmap, int);
     method public abstract void onNextTurnDistanceChanged(int, int);
     method public abstract void onStartNavigation();
@@ -689,8 +689,13 @@
   public class CarAudioManager {
     method public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method public android.media.AudioAttributes getAudioAttributesForCarUsage(int);
+    method public int getStreamMaxVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamMinVolume(int) throws android.car.CarNotConnectedException;
+    method public int getStreamVolume(int) throws android.car.CarNotConnectedException;
     method public void onCarDisconnected();
     method public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException;
+    method public void setStreamVolume(int, int, int) throws android.car.CarNotConnectedException;
+    method public void setVolumeController(android.media.IVolumeController) throws android.car.CarNotConnectedException;
     field public static final int CAR_AUDIO_USAGE_ALARM = 6; // 0x6
     field public static final int CAR_AUDIO_USAGE_DEFAULT = 0; // 0x0
     field public static final int CAR_AUDIO_USAGE_MUSIC = 1; // 0x1
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 5d56948..b6f7cc8 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -113,6 +113,12 @@
     public static final String PERMISSION_SPEED = "android.car.permission.CAR_SPEED";
 
     /**
+     * Permission necessary to change car audio volume through {@link CarAudioManager}.
+     */
+    public static final String PERMISSION_CAR_CONTROL_AUDIO_VOLUME =
+            "android.car.permission.CAR_CONTROL_AUDIO_VOLUME";
+
+    /**
      * Permission necessary to use {@link CarNavigationManager}.
      * @hide
      */
@@ -155,8 +161,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-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
index 4e8a670..858613d 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
@@ -20,7 +20,6 @@
 import android.annotation.UiThread;
 import android.car.navigation.CarNavigationInstrumentCluster;
 import android.content.Context;
-import android.view.View;
 
 /**
  * Interface for instrument cluster rendering.
@@ -39,21 +38,10 @@
      */
     abstract public void onCreate(Context context);
 
-    @UiThread
-    abstract public View onCreateView(DisplayConfiguration displayConfiguration);
-
-    @UiThread
     abstract public void onStart();
 
-    @UiThread
     abstract public void onStop();
 
-    /**
-     * Returns properties of instrument cluster for navigation.
-     */
-    abstract public CarNavigationInstrumentCluster getNavigationProperties();
-
-    @UiThread
     abstract protected NavigationRenderer createNavigationRenderer();
 
     /** The method is thread-safe, callers should cache returned object. */
diff --git a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
index 67eac5e..3df2ab1 100644
--- a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
+++ b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
@@ -17,6 +17,7 @@
 
 import android.annotation.SystemApi;
 import android.annotation.UiThread;
+import android.car.navigation.CarNavigationInstrumentCluster;
 import android.graphics.Bitmap;
 
 /**
@@ -28,6 +29,11 @@
 @SystemApi
 @UiThread
 public abstract class NavigationRenderer {
+    /**
+     * Returns properties of instrument cluster for navigation.
+     */
+    abstract public CarNavigationInstrumentCluster getNavigationProperties();
+
     abstract public void onStartNavigation();
     abstract public void onStopNavigation();
     abstract public void onNextTurnChanged(int event, String road, int turnAngle, int turnNumber,
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index 0a92c04..be4bfba 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -16,13 +16,18 @@
 package android.car.media;
 
 import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.car.CarLibLog;
+import android.car.CarNotConnectedException;
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.IVolumeController;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.car.CarManagerBase;
+import android.util.Log;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -127,6 +132,103 @@
         return mAudioManager.abandonAudioFocus(l, aa);
     }
 
+    /**
+     * Sets the volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param streamType The stream whose volume index should be set.
+     * @param index The volume index to set. See
+     *            {@link #getStreamMaxVolume(int)} for the largest valid value.
+     * @param flags One or more flags (e.g., {@link android.media.AudioManager#FLAG_SHOW_UI},
+     *              {@link android.media.AudioManager#FLAG_PLAY_SOUND})
+     */
+    @SystemApi
+    public void setStreamVolume(int streamType, int index, int flags)
+            throws CarNotConnectedException {
+        try {
+            mService.setStreamVolume(streamType, index, flags);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "setStreamVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Registers a global volume controller interface.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void setVolumeController(IVolumeController controller)
+            throws CarNotConnectedException {
+        try {
+            mService.setVolumeController(controller);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "setVolumeController failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the maximum volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose maximum volume index is returned.
+     * @return The maximum valid volume index for the stream.
+     */
+    @SystemApi
+    public int getStreamMaxVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamMaxVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamMaxVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the minimum volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose maximum volume index is returned.
+     * @return The maximum valid volume index for the stream.
+     */
+    @SystemApi
+    public int getStreamMinVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamMinVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamMaxVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Returns the current volume index for a particular stream.
+     *
+     * Requires {@link android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
+     *
+     * @param stream The stream type whose volume index is returned.
+     * @return The current volume index for the stream.
+     *
+     * @see #getStreamMaxVolume(int)
+     * @see #setStreamVolume(int, int, int)
+     */
+    @SystemApi
+    public int getStreamVolume(int stream) throws CarNotConnectedException {
+        try {
+            return mService.getStreamVolume(stream);
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "getStreamVolume failed", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
     @Override
     public void onCarDisconnected() {
         // TODO Auto-generated method stub
diff --git a/car-lib/src/android/car/media/ICarAudio.aidl b/car-lib/src/android/car/media/ICarAudio.aidl
index 7a5e43d..dfa2573 100644
--- a/car-lib/src/android/car/media/ICarAudio.aidl
+++ b/car-lib/src/android/car/media/ICarAudio.aidl
@@ -17,6 +17,7 @@
 package android.car.media;
 
 import android.media.AudioAttributes;
+import android.media.IVolumeController;
 
 /**
  * Binder interface for {@link android.car.media.CarAudioManager}.
@@ -26,4 +27,9 @@
  */
 interface ICarAudio {
     AudioAttributes getAudioAttributesForCarUsage(int carUsage) = 0;
+    void setStreamVolume(int streamType, int index, int flags) = 1;
+    void setVolumeController(IVolumeController controller) = 2;
+    int getStreamMaxVolume(int streamType) = 3;
+    int getStreamMinVolume(int streamType) = 4;
+    int getStreamVolume(int streamType) = 5;
 }
diff --git a/car-support-lib/api/current.txt b/car-support-lib/api/current.txt
index 479f056..02e3f4f 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..d628e85 100644
--- a/car-support-lib/src/android/support/car/Car.java
+++ b/car-support-lib/src/android/support/car/Car.java
@@ -17,27 +17,22 @@
 package android.support.car;
 
 import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.support.car.content.pm.CarPackageManager;
 import android.support.car.hardware.CarSensorManager;
 import android.support.car.navigation.CarNavigationManager;
-import android.util.Log;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
 
 /**
  *   Top level car API.
@@ -80,6 +75,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 +86,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 {}
 
@@ -144,33 +142,33 @@
     private int mConnectionState;
 
     private final ServiceConnectionListener mServiceConnectionListener =
-            new ServiceConnectionListener () {
-        public void onServiceConnected(ComponentName name) {
-            synchronized (Car.this) {
-                mConnectionState = STATE_CONNECTED;
-            }
-            mServiceConnectionListenerClient.onServiceConnected(name);
-        }
-
-        public void onServiceDisconnected(ComponentName name) {
-            synchronized (Car.this) {
-                if (mConnectionState  == STATE_DISCONNECTED) {
-                    return;
+            new ServiceConnectionListener() {
+                public void onServiceConnected(ComponentName name) {
+                    synchronized (Car.this) {
+                        mConnectionState = STATE_CONNECTED;
+                    }
+                    mServiceConnectionListenerClient.onServiceConnected(name);
                 }
-                mConnectionState = STATE_DISCONNECTED;
-            }
-            mServiceConnectionListenerClient.onServiceDisconnected(name);
-            connect();
-        }
 
-        public void onServiceSuspended(int cause) {
-            mServiceConnectionListenerClient.onServiceSuspended(cause);
-        }
+                public void onServiceDisconnected(ComponentName name) {
+                    synchronized (Car.this) {
+                        if (mConnectionState == STATE_DISCONNECTED) {
+                            return;
+                        }
+                        mConnectionState = STATE_DISCONNECTED;
+                    }
+                    mServiceConnectionListenerClient.onServiceDisconnected(name);
+                    connect();
+                }
 
-        public void onServiceConnectionFailed(int cause) {
-            mServiceConnectionListenerClient.onServiceConnectionFailed(cause);
-        }
-    };
+                public void onServiceSuspended(int cause) {
+                    mServiceConnectionListenerClient.onServiceSuspended(cause);
+                }
+
+                public void onServiceConnectionFailed(int cause) {
+                    mServiceConnectionListenerClient.onServiceConnectionFailed(cause);
+                }
+            };
 
     private final ServiceConnectionListener mServiceConnectionListenerClient;
     private final Object mCarManagerLock = new Object();
@@ -260,22 +258,22 @@
 
     /**
      * Car constructor when CarServiceLoader is already available.
-     * @param context
-     * @param serviceLoader
-     * @param looper
+     * @param serviceLoader must be non-null and connected or {@link CarNotConnectedException} will
+     * be thrown.
      *
      * @hide
      */
-    public Car(Context context, CarServiceLoader serviceLoader, @Nullable Looper looper) {
-        mContext = context;
-        if (looper == null) {
-            mLooper = Looper.getMainLooper();
-        } else {
-            mLooper = looper;
+    public Car(@NonNull CarServiceLoader serviceLoader)
+            throws CarNotConnectedException {
+        if(!serviceLoader.isConnectedToCar()){
+            throw new CarNotConnectedException();
         }
+        mCarServiceLoader = serviceLoader;
+        mLooper = serviceLoader.getLooper();
+        mContext = serviceLoader.getContext();
+
         mEventHandler = new Handler(mLooper);
         mConnectionState = STATE_CONNECTED;
-        mCarServiceLoader = serviceLoader;
         mServiceConnectionListenerClient = null;
     }
 
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/app/CarProxyActivity.java b/car-support-lib/src/android/support/car/app/CarProxyActivity.java
index 933e2db..4e6d6a2 100644
--- a/car-support-lib/src/android/support/car/app/CarProxyActivity.java
+++ b/car-support-lib/src/android/support/car/app/CarProxyActivity.java
@@ -23,7 +23,6 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.support.car.Car;
 import android.support.car.ServiceConnectionListener;
 import android.support.car.input.CarInputManager;
@@ -58,7 +57,7 @@
     private CarInputManager mInputManager;
 
     private final CopyOnWriteArrayList<Pair<Integer, Object[]>> mCmds =
-            new CopyOnWriteArrayList<Pair<Integer, Object[]>>();
+            new CopyOnWriteArrayList<>();
     private final ServiceConnectionListener mConnectionListener= new ServiceConnectionListener() {
 
         @Override
@@ -375,8 +374,7 @@
             mCarActivity.dispatchCmd(cmd, args);
         } else {
             // not connected yet. queue it and return.
-            Pair<Integer, Object[]> cmdToQ =
-                    new Pair<Integer, Object[]>(Integer.valueOf(cmd), args);
+            Pair<Integer, Object[]> cmdToQ = new Pair<>(Integer.valueOf(cmd), args);
             mCmds.add(cmdToQ);
         }
     }
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..83ccd5d
--- /dev/null
+++ b/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java
@@ -0,0 +1,492 @@
+/*
+ * 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);
+        }
+
+        pushSensorChanges(sensorType);
+
+        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/car_product/build/car.mk b/car_product/build/car.mk
index 3a7cfb1..aaed0d2 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -32,7 +32,7 @@
     screenrecord
 
 # This is for testing
-PRODUCT_PACKAGES +=
+PRODUCT_PACKAGES += \
     EmbeddedKitchenSinkApp
 
 PRODUCT_COPY_FILES := \
@@ -99,6 +99,10 @@
 
 PRODUCT_LOCALES := en_US af_ZA am_ET ar_EG bg_BG bn_BD ca_ES cs_CZ da_DK de_DE el_GR en_AU en_GB en_IN es_ES es_US et_EE eu_ES fa_IR fi_FI fr_CA fr_FR gl_ES hi_IN hr_HR hu_HU hy_AM in_ID is_IS it_IT iw_IL ja_JP ka_GE km_KH ko_KR ky_KG lo_LA lt_LT lv_LV km_MH kn_IN mn_MN ml_IN mk_MK mr_IN ms_MY my_MM ne_NP nb_NO nl_NL pl_PL pt_BR pt_PT ro_RO ru_RU si_LK sk_SK sl_SI sr_RS sv_SE sw_TZ ta_IN te_IN th_TH tl_PH tr_TR uk_UA vi_VN zh_CN zh_HK zh_TW zu_ZA en_XA ar_XB
 
+# should add to BOOT_JARS only once
+ifeq (,$(INCLUDED_ANDROID_CAR_TO_PRODUCT_BOOT_JARS))
 PRODUCT_BOOT_JARS += \
     android.car
 
+INCLUDED_ANDROID_CAR_TO_PRODUCT_BOOT_JARS := yes
+endif
diff --git a/car_product/init/init.bootstat.rc b/car_product/init/init.bootstat.rc
new file mode 100644
index 0000000..8c93d67
--- /dev/null
+++ b/car_product/init/init.bootstat.rc
@@ -0,0 +1,36 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# IMPORTANT: Do not create world writable files or directories.
+# This is a common source of Android security bugs.
+#
+on post-fs-data
+    mkdir /data/misc/bootstat 0700 root root
+
+# Record the time at which the user has successfully entered the pin to decrypt
+# the device, /data is decrypted, and the system is entering the main boot phase.
+#
+# post-fs-data: /data is writable
+# property:init.svc.bootanim=running: The boot animation is running
+on post-fs-data && property:init.svc.bootanim=running
+    exec - root root -- /system/bin/bootstat -r post_decrypt_time_elapsed
+
+# Boot animation stopped, is considered the point at which
+# the user may interact with the device, so it is a good proxy for the boot
+# complete signal.
+on property:init.svc.bootanim=stopped
+    # Record boot_complete and related stats (decryption, etc).
+    exec - root root -- /system/bin/bootstat --record_boot_complete
+
+on property:dev.bootcomplete=1
+    exec - root root -- /system/bin/bootstat -r dev_bootcomplete
+    # Log all boot events.
+    exec - root root -- /system/bin/bootstat -l
+
+on property:boot.car_service_created=1
+    exec - root root -- /system/bin/bootstat -r car_service_created
+
+on property:init.svc.vns=running
+    exec - root root -- /system/bin/bootstat -r vns_running
+
+on property:init.svc.zygote=running
+    exec - root root -- /system/bin/bootstat -r zygote_running
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/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehiclePropConfigUtil.java b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehiclePropConfigUtil.java
index 61e675b..2482f39 100644
--- a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehiclePropConfigUtil.java
+++ b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehiclePropConfigUtil.java
@@ -21,6 +21,9 @@
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Utility class to help creating VehiclePropConfig.
  */
@@ -66,6 +69,14 @@
     public static VehiclePropConfig.Builder getBuilder(int property, int access, int changeMode,
             int type, int permissionModel, int configFlags, float sampleRateMax,
             float sampleRateMin) {
+        List<Integer> emptyList = new ArrayList<Integer>();
+        return getBuilder(property, access, changeMode, type, permissionModel, configFlags,
+                sampleRateMax, sampleRateMin, emptyList, emptyList);
+    }
+
+    public static VehiclePropConfig.Builder getBuilder(int property, int access, int changeMode,
+            int type, int permissionModel, int configFlags, float sampleRateMax,
+            float sampleRateMin, List<Integer> int32Maxs, List<Integer> int32Mins) {
         return VehiclePropConfig.newBuilder().
                 setProp(property).
                 setAccess(access).
@@ -74,6 +85,8 @@
                 setPermissionModel(permissionModel).
                 addConfigArray(configFlags).
                 setSampleRateMax(sampleRateMax).
-                setSampleRateMin(sampleRateMin);
+                setSampleRateMin(sampleRateMin)
+                .addAllInt32Maxs(int32Maxs)
+                .addAllInt32Mins(int32Mins);
     }
 }
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 73dd518..4df72e4 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -88,6 +88,12 @@
         android:label="@string/car_permission_label_control_app_blocking"
         android:description="@string/car_permission_desc_control_app_blocking" />
 
+    <permission
+        android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"
+        android:protectionLevel="system|signature"
+        android:label="@string/car_permission_label_audio_volume"
+        android:description="@string/car_permission_desc_audio_volume" />
+
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.DEVICE_POWER" />
diff --git a/service/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/drawable-hdpi/car_ic_error.png b/service/res/drawable-hdpi/car_ic_error.png
new file mode 100644
index 0000000..ebb027d
--- /dev/null
+++ b/service/res/drawable-hdpi/car_ic_error.png
Binary files differ
diff --git a/service/res/drawable-mdpi/car_ic_error.png b/service/res/drawable-mdpi/car_ic_error.png
new file mode 100644
index 0000000..6d6c10f
--- /dev/null
+++ b/service/res/drawable-mdpi/car_ic_error.png
Binary files differ
diff --git a/service/res/drawable-xhdpi/car_ic_error.png b/service/res/drawable-xhdpi/car_ic_error.png
new file mode 100644
index 0000000..921555d
--- /dev/null
+++ b/service/res/drawable-xhdpi/car_ic_error.png
Binary files differ
diff --git a/service/res/drawable-xxhdpi/car_ic_error.png b/service/res/drawable-xxhdpi/car_ic_error.png
new file mode 100644
index 0000000..dfab63c
--- /dev/null
+++ b/service/res/drawable-xxhdpi/car_ic_error.png
Binary files differ
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 24ef730..3fd2a62 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -33,20 +33,30 @@
          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>
+    <!--  Configuration to enable usage of dynamic audio routing. If this is set to false,
+          dynamic audio routing is disabled and audio works in legacy mode. It may be useful
+          during initial development where audio hal does not support bus based addressing yet. -->
+    <bool name="audioUseDynamicRouting">true</bool>
+
+    <!-- Number of audio focus timeouts that indicate vehicle CAN bus failure. -->
+    <integer name="consecutiveHalFailures">3</integer>
 
     <!-- This is kernel device node to allow input event injection for key inputs coming
          from vehicle hal -->
     <string name="inputInjectionDeviceNode">/dev/input/event2</string>
 
-    <string name="instrumentClusterRendererPackage">android.car.cluster.demorenderer</string>
-    <string name="instrumentClusterRendererFactoryClass">android.car.cluster.demorenderer.InstrumentClusterRendererFactory</string>
+    <string name="instrumentClusterRendererPackage">android.car.cluster.loggingrenderer</string>
+    <string name="instrumentClusterRendererFactoryClass">android.car.cluster.loggingrenderer.InstrumentClusterRendererFactory</string>
 </resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 2fac6b4..3721fb3 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -50,12 +50,16 @@
     <string name="car_permission_desc_radio">Access your car\'s radio.</string>
     <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_projection">Car Projection</string>
+    <!-- Permission text: apps can control car-audio-volume [CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_audio_volume">Car Audio Volume</string>
     <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_projection">Project phone interface on car display.</string>
     <string name="car_permission_label_mock_vehicle_hal">Emulate vehicle HAL</string>
     <!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_mock_vehicle_hal">Emulate your car\'s vehicle HAL for internal
         testing purpose.</string>
+    <!-- Permission text: can adjust the audio volume on your car [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_audio_volume">Control your car\'s audio volume.</string>
     <string name="car_permission_label_control_app_blocking">Application blocking</string>
     <!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_control_app_blocking">Control application blocking while
@@ -65,4 +69,11 @@
          cluster [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_car_navigation_manager">Report navigation data to instrument
         cluster</string>
+
+    <!-- Notification messages -->
+    <!-- Notification text: Notification shown to the user when vehicle CAN bus fails -->
+    <string name="car_can_bus_failure">CAN bus failed</string>
+    <!-- Notification text: Notification description shown to the user when vehicle CAN bus fails -->
+    <string name="car_can_bus_failure_desc">CAN bus does not respond. Unplug and plug back headunit
+        box and restart the car</string>
 </resources>
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/CanBusErrorNotifier.java b/service/src/com/android/car/CanBusErrorNotifier.java
new file mode 100644
index 0000000..115dda0
--- /dev/null
+++ b/service/src/com/android/car/CanBusErrorNotifier.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Class used to notify user about CAN bus failure.
+ */
+final class CanBusErrorNotifier {
+    private static final String TAG = CarLog.TAG_CAN_BUS + ".ERROR.NOTIFIER";
+    private static final int NOTIFICATION_ID = 1;
+    private static final boolean IS_RELEASE_BUILD = "user".equals(Build.TYPE);
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+
+    @GuardedBy("this")
+    private boolean mFailed;
+
+    @GuardedBy("this")
+    private Notification mNotification;
+
+    CanBusErrorNotifier(Context context) {
+        mNotificationManager = (NotificationManager) context.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        mContext = context;
+    }
+
+    public void setCanBusFailure(boolean failed) {
+        synchronized (this) {
+            if (mFailed == failed) {
+                return;
+            }
+            mFailed = failed;
+        }
+        Log.i(TAG, "Changing CAN bus failure state to " + failed);
+
+        if (failed) {
+            showNotification();
+        } else {
+            hideNotification();
+        }
+    }
+
+    private void showNotification() {
+        if (IS_RELEASE_BUILD) {
+            // TODO: for release build we should show message to take car to the dealer.
+            return;
+        }
+        Notification notification;
+        synchronized (this) {
+            if (mNotification == null) {
+                mNotification = new Notification.Builder(mContext)
+                        .setContentTitle(mContext.getString(R.string.car_can_bus_failure))
+                        .setContentText(mContext.getString(R.string.car_can_bus_failure_desc))
+                        .setSmallIcon(R.drawable.car_ic_error)
+                        .setOngoing(true)
+                        .build();
+            }
+            notification = mNotification;
+        }
+        mNotificationManager.notify(TAG, NOTIFICATION_ID, notification);
+    }
+
+    private void hideNotification() {
+        if (IS_RELEASE_BUILD) {
+            // TODO: for release build we should show message to take car to the dealer.
+            return;
+        }
+        mNotificationManager.cancel(TAG, NOTIFICATION_ID);
+    }
+}
diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java
index 5019fb4..b5a0a09 100644
--- a/service/src/com/android/car/CarAudioService.java
+++ b/service/src/com/android/car/CarAudioService.java
@@ -15,46 +15,71 @@
  */
 package com.android.car;
 
+import android.car.Car;
+import android.car.VehicleZoneUtil;
+import android.app.AppGlobals;
 import android.car.media.CarAudioManager;
 import android.car.media.ICarAudio;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.media.AudioAttributes;
+import android.media.AudioDeviceInfo;
 import android.media.AudioFocusInfo;
+import android.media.AudioFormat;
 import android.media.AudioManager;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.IVolumeController;
 import android.media.audiopolicy.AudioPolicy;
 import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.car.hal.AudioHalService;
-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;
 
 import java.io.PrintWriter;
 import java.util.LinkedList;
 
+public class CarAudioService extends ICarAudio.Stub implements CarServiceBase,
+        AudioHalFocusListener {
 
-public class CarAudioService extends ICarAudio.Stub implements CarServiceBase, AudioHalListener {
+    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 static final long FOCUS_RESPONSE_WAIT_TIMEOUT_MS = 1000;
+    private final long mFocusResponseWaitTimeoutMs;
+
+    private final int mNumConsecutiveHalFailuresForCanError;
 
     private static final String TAG_FOCUS = CarLog.TAG_AUDIO + ".FOCUS";
 
     private static final boolean DBG = true;
+    private static final boolean DBG_DYNAMIC_AUDIO_ROUTING = true;
 
     private final AudioHalService mAudioHal;
     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 CarVolumeService mVolumeService;
     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")
@@ -71,6 +96,7 @@
 
     private AudioRoutingPolicy mAudioRoutingPolicy;
     private final AudioManager mAudioManager;
+    private final CanBusErrorNotifier mCanBusErrorNotifier;
     private final BottomAudioFocusListener mBottomAudioFocusHandler =
             new BottomAudioFocusListener();
     private final CarProxyAndroidFocusListener mCarProxyAudioFocusHandler =
@@ -83,6 +109,20 @@
     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;
+    @GuardedBy("mLock")
+    private int mNumConsecutiveHalFailures;
+
+    private final boolean mUseDynamicRouting;
 
     private final AudioAttributes mAttributeBottom =
             CarAudioAttributesUtil.getAudioAttributesForCarUsage(
@@ -91,15 +131,21 @@
             CarAudioAttributesUtil.getAudioAttributesForCarUsage(
                     CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY);
 
-    public CarAudioService(Context context) {
+    public CarAudioService(Context context, CarInputService inputService) {
         mAudioHal = VehicleHal.getInstance().getAudioHal();
         mContext = context;
         mFocusHandlerThread = new HandlerThread(CarLog.TAG_AUDIO);
         mSystemFocusListener = new SystemFocusListener();
         mFocusHandlerThread.start();
         mFocusHandler = new CarAudioFocusChangeHandler(mFocusHandlerThread.getLooper());
-        mVolumeHandler = new CarAudioVolumeHandler(Looper.getMainLooper());
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mCanBusErrorNotifier = new CanBusErrorNotifier(context);
+        Resources res = context.getResources();
+        mFocusResponseWaitTimeoutMs = (long) res.getInteger(R.integer.audioFocusWaitTimeoutMs);
+        mNumConsecutiveHalFailuresForCanError =
+                (int) res.getInteger(R.integer.consecutiveHalFailures);
+        mUseDynamicRouting = res.getBoolean(R.bool.audioUseDynamicRouting);
+        mVolumeService = new CarVolumeService(mContext, this, mAudioHal, inputService);
     }
 
     @Override
@@ -111,12 +157,9 @@
     public void init() {
         AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
         builder.setLooper(Looper.getMainLooper());
-        boolean isFocusSuported = mAudioHal.isFocusSupported();
-        if (isFocusSuported) {
+        boolean isFocusSupported = mAudioHal.isFocusSupported();
+        if (isFocusSupported) {
             builder.setAudioPolicyFocusListener(mSystemFocusListener);
-        }
-        mAudioPolicy = builder.build();
-        if (isFocusSuported) {
             FocusState currentState = FocusState.create(mAudioHal.getCurrentFocusState());
             int r = mAudioManager.requestAudioFocus(mBottomAudioFocusHandler, mAttributeBottom,
                     AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
@@ -130,31 +173,177 @@
                 mCurrentAudioContexts = 0;
             }
         }
-        int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
-        if (r != 0) {
-            throw new RuntimeException("registerAudioPolicy failed " + r);
-        }
-        mAudioHal.setListener(this);
         int audioHwVariant = mAudioHal.getHwVariant();
-        mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
-        mAudioHal.setAudioRoutingPolicy(mAudioRoutingPolicy);
-        //TODO set routing policy with new AudioPolicy API. This will control which logical stream
-        //     goes to which physical stream.
+        AudioRoutingPolicy audioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
+        if (mUseDynamicRouting) {
+            setupDynamicRoutng(audioRoutingPolicy, builder);
+        }
+        AudioPolicy audioPolicy = null;
+        if (isFocusSupported || mUseDynamicRouting) {
+            audioPolicy = builder.build();
+            int r = mAudioManager.registerAudioPolicy(audioPolicy);
+            if (r != 0) {
+                throw new RuntimeException("registerAudioPolicy failed " + r);
+            }
+        }
+        mAudioHal.setFocusListener(this);
+        mAudioHal.setAudioRoutingPolicy(audioRoutingPolicy);
+        synchronized (mLock) {
+            if (audioPolicy != null) {
+                mAudioPolicy = audioPolicy;
+            }
+            mAudioRoutingPolicy = audioRoutingPolicy;
+            mIsRadioExternal = mAudioHal.isRadioExternal();
+        }
+        mVolumeService.init();
+    }
+
+    private void setupDynamicRoutng(AudioRoutingPolicy audioRoutingPolicy,
+            AudioPolicy.Builder audioPolicyBuilder) {
+        AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+        if (deviceInfos.length == 0) {
+            Log.e(CarLog.TAG_AUDIO, "setupDynamicRoutng, no output device available, ignore");
+            return;
+        }
+        int numPhysicalStreams = audioRoutingPolicy.getPhysicalStreamsCount();
+        AudioDeviceInfo[] devicesToRoute = new AudioDeviceInfo[numPhysicalStreams];
+        for (AudioDeviceInfo info : deviceInfos) {
+            if (DBG_DYNAMIC_AUDIO_ROUTING) {
+                Log.v(CarLog.TAG_AUDIO, String.format(
+                        "output device=%s id=%d name=%s addr=%s type=%s",
+                        info.toString(), info.getId(), info.getProductName(), info.getAddress(),
+                        info.getType()));
+            }
+            if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
+                int addressNumeric = parseDeviceAddress(info.getAddress());
+                if (addressNumeric >= 0 && addressNumeric < numPhysicalStreams) {
+                    devicesToRoute[addressNumeric] = info;
+                    Log.i(CarLog.TAG_AUDIO, String.format(
+                            "valid bus found, devie=%s id=%d name=%s addr=%s",
+                            info.toString(), info.getId(), info.getProductName(), info.getAddress())
+                            );
+                }
+            }
+        }
+        for (int i = 0; i < numPhysicalStreams; i++) {
+            AudioDeviceInfo info = devicesToRoute[i];
+            if (info == null) {
+                Log.e(CarLog.TAG_AUDIO, "setupDynamicRoutng, cannot find device for address " + i);
+                return;
+            }
+            int sampleRate = getMaxSampleRate(info);
+            int channels = getMaxChannles(info);
+            AudioFormat mixFormat = new AudioFormat.Builder()
+                .setSampleRate(sampleRate)
+                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                .setChannelMask(channels)
+                .build();
+            Log.i(CarLog.TAG_AUDIO, String.format(
+                    "Physical stream %d, sampleRate:%d, channles:0x%s", i, sampleRate,
+                    Integer.toHexString(channels)));
+            int[] logicalStreams = audioRoutingPolicy.getLogicalStreamsForPhysicalStream(i);
+            AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
+            for (int logicalStream : logicalStreams) {
+                mixingRuleBuilder.addRule(
+                        CarAudioAttributesUtil.getAudioAttributesForCarUsage(logicalStream),
+                        AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+            }
+            AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
+                .setFormat(mixFormat)
+                .setDevice(info)
+                .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
+                .build();
+            audioPolicyBuilder.addMix(audioMix);
+        }
+    }
+
+    /**
+     * Parse device address. Expected format is BUS%d_%s, address, usage hint
+     * @return valid address (from 0 to positive) or -1 for invalid address.
+     */
+    private int parseDeviceAddress(String address) {
+        String[] words = address.split("_");
+        int addressParsed = -1;
+        if (words[0].startsWith("BUS")) {
+            try {
+                addressParsed = Integer.parseInt(words[0].substring(3));
+            } catch (NumberFormatException e) {
+                //ignore
+            }
+        }
+        if (addressParsed < 0) {
+            return -1;
+        }
+        return addressParsed;
+    }
+
+    private int getMaxSampleRate(AudioDeviceInfo info) {
+        int[] sampleRates = info.getSampleRates();
+        if (sampleRates == null || sampleRates.length == 0) {
+            return 48000;
+        }
+        int sampleRate = sampleRates[0];
+        for (int i = 1; i < sampleRates.length; i++) {
+            if (sampleRates[i] > sampleRate) {
+                sampleRate = sampleRates[i];
+            }
+        }
+        return sampleRate;
+    }
+
+    private int getMaxChannles(AudioDeviceInfo info) {
+        int[] channelMasks = info.getChannelMasks();
+        if (channelMasks == null) {
+            return AudioFormat.CHANNEL_OUT_STEREO;
+        }
+        int channels = AudioFormat.CHANNEL_OUT_MONO;
+        int numChannels = 1;
+        for (int i = 0; i < channelMasks.length; i++) {
+            int currentNumChannles = VehicleZoneUtil.getNumberOfZones(channelMasks[i]);
+            if (currentNumChannles > numChannels) {
+                numChannels = currentNumChannles;
+                channels = channelMasks[i];
+            }
+        }
+        return channels;
     }
 
     @Override
     public void release() {
-        mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
+        mFocusHandler.cancelAll();
         mAudioManager.abandonAudioFocus(mBottomAudioFocusHandler);
         mAudioManager.abandonAudioFocus(mCarProxyAudioFocusHandler);
-        mFocusHandler.cancelAll();
+        AudioPolicy audioPolicy;
         synchronized (mLock) {
             mCurrentFocusState = FocusState.STATE_LOSS;
             mLastFocusRequestToCar = null;
             mTopFocusInfo = null;
             mPendingFocusChanges.clear();
             mRadioActive = false;
+            if (mCarAudioContextChangeHandler != null) {
+                mCarAudioContextChangeHandler.cancelAll();
+                mCarAudioContextChangeHandler = null;
+            }
+            mAudioContextChangeListener = null;
+            mCurrentPrimaryAudioContext = 0;
+            audioPolicy = mAudioPolicy;
+            mAudioPolicy = null;
         }
+        if (audioPolicy != null) {
+            mAudioManager.unregisterAudioPolicyAsync(audioPolicy);
+        }
+    }
+
+    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
@@ -164,6 +353,10 @@
                 " 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);
+        writer.println(" mNumConsecutiveHalFailures:" + mNumConsecutiveHalFailures);
         mAudioRoutingPolicy.dump(writer);
     }
 
@@ -178,21 +371,52 @@
     }
 
     @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);
     }
 
+    @Override
+    public void setStreamVolume(int streamType, int index, int flags) {
+        enforceAudioVolumePermission();
+        mVolumeService.setStreamVolume(streamType, index, flags);
+    }
+
+    @Override
+    public void setVolumeController(IVolumeController controller) {
+        enforceAudioVolumePermission();
+        mVolumeService.setVolumeController(controller);
+    }
+
+    @Override
+    public int getStreamMaxVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamMaxVolume(streamType);
+    }
+
+    @Override
+    public int getStreamMinVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamMinVolume(streamType);
+    }
+
+    @Override
+    public int getStreamVolume(int streamType) {
+        enforceAudioVolumePermission();
+        return mVolumeService.getStreamVolume(streamType);
+    }
+
+    public AudioRoutingPolicy getAudioRoutingPolicy() {
+        return mAudioRoutingPolicy;
+    }
+
+    private void enforceAudioVolumePermission() {
+        if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "requires permission " + Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+        }
+    }
+
     private void doHandleCarFocusChange() {
         int newFocusState = AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_INVALID;
         FocusState currentState;
@@ -299,14 +523,8 @@
             shouldRequestProxyFocus = true;
         }
         if (isFocusFromCarProxy(topInfo)) {
-            if ((currentState.externalFocus &
-                    (AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_PERMANENT_FLAG |
-                            AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_TRANSIENT_FLAG)) == 0) {
-                // CarProxy in top, but no external focus: Drop it so that some other app
-                // may pick up focus.
-                mAudioManager.abandonAudioFocus(mCarProxyAudioFocusHandler);
-                return;
-            }
+            // already car proxy is top. Nothing to do.
+            return;
         } else if (!isFocusFromCarServiceBottom(topInfo)) {
             shouldRequestProxyFocus = true;
         }
@@ -333,10 +551,6 @@
                 androidFocus, flags, mAudioPolicy);
     }
 
-    private void doHandleVolumeChange(VolumeStateChangeEvent event) {
-        //TODO
-    }
-
     private void doHandleStreamStatusChange(int streamNumber, int state) {
         //TODO
     }
@@ -368,8 +582,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 +644,24 @@
         int logicalStreamTypeForTop = CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib);
         int physicalStreamTypeForTop = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
                 logicalStreamTypeForTop);
+
+        // update primary context and notify if necessary
+        int primaryContext = AudioHalService.logicalStreamToHalContextType(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 +672,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 +681,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 +725,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 +773,7 @@
             mAudioHal.requestAudioFocusChange(focusToRequest, streamsToRequest, extFocus,
                     audioContexts);
             try {
-                mLock.wait(FOCUS_RESPONSE_WAIT_TIMEOUT_MS);
+                mLock.wait(mFocusResponseWaitTimeoutMs);
             } catch (InterruptedException e) {
                 //ignore
             }
@@ -630,12 +861,18 @@
                             dumpAudioFocusInfo(newTopInfo) + " currentState:" + mCurrentFocusState);
                 }
             }
-            if (focusRequested && mFocusReceived == null) {
-                Log.w(TAG_FOCUS, "focus response timed out, request sent" +
-                        mLastFocusRequestToCar);
-                // no response. so reset to loss.
-                mFocusReceived = FocusState.STATE_LOSS;
-                mCurrentAudioContexts = 0;
+            if (focusRequested) {
+                if (mFocusReceived == null) {
+                    Log.w(TAG_FOCUS, "focus response timed out, request sent"
+                            + mLastFocusRequestToCar);
+                    // no response. so reset to loss.
+                    mFocusReceived = FocusState.STATE_LOSS;
+                    mCurrentAudioContexts = 0;
+                    mNumConsecutiveHalFailures++;
+                } else {
+                    mNumConsecutiveHalFailures = 0;
+                }
+                checkCanStatus();
             }
         }
         // handle it if there was response or force handle it for timeout.
@@ -657,7 +894,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
                 }
@@ -671,6 +908,12 @@
         }
     }
 
+    private void checkCanStatus() {
+        // If CAN bus recovers, message will be removed.
+        mCanBusErrorNotifier.setCanBusFailure(
+                mNumConsecutiveHalFailures >= mNumConsecutiveHalFailuresForCanError);
+    }
+
     private static boolean isAudioAttributesSame(AudioAttributes one, AudioAttributes two) {
         if (one.getContentType() != two.getContentType()) {
             return false;
@@ -748,6 +991,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 +1089,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..27e72dc 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -35,18 +35,20 @@
 public class CarInputService implements CarServiceBase, InputHalService.InputListener {
 
     public interface KeyEventListener {
-        void onKeyEvent(KeyEvent event);
+        boolean onKeyEvent(KeyEvent event);
     }
 
     private static final long VOICE_LONG_PRESS_TIME_MS = 1000;
 
     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,7 @@
     private void handleInstrumentClusterKey(KeyEvent event) {
         KeyEventListener listener = null;
         synchronized (this) {
-            listener = mInstumentClusterKeyListener;
+            listener = mInstrumentClusterKeyListener;
         }
         if (listener == null) {
             return;
@@ -197,6 +209,16 @@
         listener.onKeyEvent(event);
     }
 
+    private void handleVolumeKey(KeyEvent event) {
+        KeyEventListener listener;
+        synchronized (this) {
+            listener = mVolumeKeyListener;
+        }
+        if (listener != null) {
+            listener.onKeyEvent(event);
+        }
+    }
+
     private void handleMainDisplayKey(KeyEvent event) {
         int fd;
         synchronized (this) {
diff --git a/service/src/com/android/car/CarLog.java b/service/src/com/android/car/CarLog.java
index 20c4b06..79ed606 100644
--- a/service/src/com/android/car/CarLog.java
+++ b/service/src/com/android/car/CarLog.java
@@ -33,4 +33,6 @@
     public static final String TAG_INPUT = "CAR.INPUT";
     public static final String TAG_PROJECTION = "CAR.PROJECTION";
     public static final String TAG_CLUSTER = "CAR.CLUSTER";
+    public static final String TAG_CAN_BUS = "CAR.CAN_BUS";
+    public static final String TAG_SYS = "CAR.SYS";
 }
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index 819e094..c6d0191 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -177,6 +177,7 @@
             onApPowerStateChange(currentState);
         } else {
             Log.w(CarLog.TAG_POWER, "Vehicle hal does not support power state yet.");
+            onApPowerStateChange(new PowerState(PowerHalService.STATE_ON_FULL, 0));
             mSystemInterface.switchToFullWakeLock();
         }
         mSystemInterface.startDisplayStateMonitoring(this);
@@ -407,9 +408,6 @@
             listener.onSleepEntry();
         }
         int wakeupTimeSec = getWakeupTime();
-        for (PowerEventProcessingHandlerWrapper wrapper : mPowerEventProcessingHandlers) {
-            wrapper.resetPowerOnSent();
-        }
         mHal.sendSleepEntry();
         synchronized (this) {
             mLastSleepEntryTime = SystemClock.elapsedRealtime();
@@ -440,7 +438,7 @@
     private void doHandleNotifyPowerOn() {
         boolean displayOn = false;
         synchronized (this) {
-            if (mCurrentState != null && mCurrentState.state == PowerHalService.SET_DISPLAY_ON) {
+            if (mCurrentState != null && mCurrentState.state == PowerHalService.STATE_ON_FULL) {
                 displayOn = true;
             }
         }
@@ -784,6 +782,7 @@
         private long mProcessingTime = 0;
         private boolean mProcessingDone = true;
         private boolean mPowerOnSent = false;
+        private int mLastDisplayState = -1;
 
         public PowerEventProcessingHandlerWrapper(PowerEventProcessingHandler handler) {
             this.handler = handler;
@@ -807,11 +806,13 @@
         }
 
         public void callOnPowerOn(boolean displayOn) {
+            int newDisplayState = displayOn ? 1 : 0;
             boolean shouldCall = false;
             synchronized (this) {
-                if (!mPowerOnSent) {
+                if (!mPowerOnSent || (mLastDisplayState != newDisplayState)) {
                     shouldCall = true;
                     mPowerOnSent = true;
+                    mLastDisplayState = newDisplayState;
                 }
             }
             if (shouldCall) {
@@ -819,10 +820,6 @@
             }
         }
 
-        public synchronized void resetPowerOnSent() {
-            mPowerOnSent = false;
-        }
-
         @Override
         public String toString() {
             return "PowerEventProcessingHandlerWrapper [handler=" + handler + ", mProcessingTime="
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 66c75fa..1c5f15d 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;
@@ -47,16 +44,18 @@
     private final CarInputService.KeyEventListener mVoiceAssistantKeyListener =
             new CarInputService.KeyEventListener() {
                 @Override
-                public void onKeyEvent(KeyEvent event) {
+                public boolean onKeyEvent(KeyEvent event) {
                     handleVoiceAssitantRequest(false);
+                    return true;
                 }
             };
 
     private final CarInputService.KeyEventListener mLongVoiceAssistantKeyListener =
             new CarInputService.KeyEventListener() {
                 @Override
-                public void onKeyEvent(KeyEvent event) {
+                public boolean onKeyEvent(KeyEvent event) {
                     handleVoiceAssitantRequest(true);
+                    return true;
                 }
             };
 
@@ -184,9 +183,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/CarService.java b/service/src/com/android/car/CarService.java
index e0feb8a..8986206 100644
--- a/service/src/com/android/car/CarService.java
+++ b/service/src/com/android/car/CarService.java
@@ -24,6 +24,7 @@
 import android.car.Car;
 import android.content.Intent;
 import android.os.IBinder;
+import android.os.SystemProperties;
 import android.util.Log;
 
 public class CarService extends Service {
@@ -35,6 +36,7 @@
     public void onCreate() {
         Log.i(CarLog.TAG_SERVICE, "Service onCreate");
         mICarImpl = ICarImpl.getInstance(this);
+        SystemProperties.set("boot.car_service_created", "1");
         super.onCreate();
     }
 
diff --git a/service/src/com/android/car/CarVolumeControllerFactory.java b/service/src/com/android/car/CarVolumeControllerFactory.java
new file mode 100644
index 0000000..72cf310
--- /dev/null
+++ b/service/src/com/android/car/CarVolumeControllerFactory.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.IVolumeController;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+
+import com.android.car.CarVolumeService.CarVolumeController;
+import com.android.car.hal.AudioHalService;
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based
+ * on car properties.
+ */
+public class CarVolumeControllerFactory {
+
+    public static CarVolumeController createCarVolumeController(Context context,
+            CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) {
+        final boolean volumeSupported = audioHal.isAudioVolumeSupported();
+
+        // Case 1: Car Audio Module does not support volume controls
+        if (!volumeSupported) {
+            return new SimpleCarVolumeController(context);
+        }
+        return new CarExternalVolumeController(context, audioService, audioHal, inputService);
+    }
+
+    /**
+     * To control volumes through {@link android.media.AudioManager} when car audio module does not
+     * support volume controls.
+     */
+    public static final class SimpleCarVolumeController extends CarVolumeController {
+        private final AudioManager mAudioManager;
+        private final Context mContext;
+
+        public SimpleCarVolumeController(Context context) {
+            mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+            mContext = context;
+        }
+
+        @Override
+        void init() {
+        }
+
+        @Override
+        public void setStreamVolume(int stream, int index, int flags) {
+            mAudioManager.setStreamVolume(stream, index, flags);
+        }
+
+        @Override
+        public int getStreamVolume(int stream) {
+            return mAudioManager.getStreamVolume(stream);
+        }
+
+        @Override
+        public void setVolumeController(IVolumeController controller) {
+            mAudioManager.setVolumeController(controller);
+        }
+
+        @Override
+        public int getStreamMaxVolume(int stream) {
+            return mAudioManager.getStreamMaxVolume(stream);
+        }
+
+        @Override
+        public int getStreamMinVolume(int stream) {
+            return mAudioManager.getStreamMinVolume(stream);
+        }
+
+        @Override
+        public boolean onKeyEvent(KeyEvent event) {
+            handleVolumeKeyDefault(event);
+            return true;
+        }
+
+        private void handleVolumeKeyDefault(KeyEvent event) {
+            if (event.getAction() != KeyEvent.ACTION_DOWN) {
+                return;
+            }
+
+            boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP;
+            int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND
+                    | AudioManager.FLAG_FROM_KEY;
+            IAudioService audioService = getAudioService();
+            String pkgName = mContext.getOpPackageName();
+            try {
+                if (audioService != null) {
+                    audioService.adjustSuggestedStreamVolume(
+                            volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER,
+                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT);
+                }
+            } catch (RemoteException e) {
+                Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e);
+            }
+        }
+
+        private static IAudioService getAudioService() {
+            IAudioService audioService = IAudioService.Stub.asInterface(
+                    ServiceManager.checkService(Context.AUDIO_SERVICE));
+            if (audioService == null) {
+                Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface.");
+            }
+            return audioService;
+        }
+    }
+
+    /**
+     * The car volume controller to use when the car audio modules supports volume controls.
+     *
+     * Depending on whether the car support audio context and has persistent memory, we need to
+     * handle per context volume change properly.
+     *
+     * Regardless whether car supports audio context or not, we need to keep per audio context
+     * volume internally. If we only support single channel, then we only send the volume change
+     * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on
+     * software mixer level or send it the car audio module if the car support audio context
+     * and multi channel. TODO: Add support for multi channel.
+     *
+     * Per context volume should be persisted, so the volumes can stay the same across boots.
+     * Depending on the hardware property, this can be persisted on car side (or/and android side).
+     * TODO: we need to define one single source of truth if the car has memory.
+     */
+    public static class CarExternalVolumeController extends CarVolumeController
+            implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener,
+            CarAudioService.AudioContextChangeListener {
+        private static final String TAG = CarLog.TAG_AUDIO + "ExtVolCtrl";
+        private static final int MSG_UPDATE_VOLUME = 0;
+        private static final int MSG_UPDATE_HAL = 1;
+
+        private final Context mContext;
+        private final AudioRoutingPolicy mPolicy;
+        private final AudioHalService mHal;
+        private final CarInputService mInputService;
+        private final CarAudioService mAudioService;
+
+        private int mSupportedAudioContext;
+
+        private boolean mHasExternalMemory;
+
+        @GuardedBy("this")
+        private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
+        // current logical volume, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mCurrentLogicalVolume =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+        // stream volume limit, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mLogicalStreamVolumeMax =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+        // stream volume limit, the key is android stream type
+        @GuardedBy("this")
+        private final SparseArray<Integer> mLogicalStreamVolumeMin =
+                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
+
+        @GuardedBy("this")
+        private final RemoteCallbackList<IVolumeController> mVolumeControllers =
+                new RemoteCallbackList<>();
+
+        private final Handler mHandler = new VolumeHandler();
+
+        /**
+         * Convert an android logical stream to the car stream.
+         *
+         * @return If car supports audio context, then it returns the car audio context. Otherwise,
+         *      it returns the physical stream that maps to this logical stream.
+         */
+        private int logicalStreamToCarStream(int logicalAndroidStream) {
+            if (mSupportedAudioContext == 0) {
+                int physicalStream = mPolicy.getPhysicalStreamForLogicalStream(
+                        CarVolumeService.androidStreamToCarUsage(logicalAndroidStream));
+                return physicalStream;
+            } else {
+                int carContext = VolumeUtils.androidStreamToCarContext(logicalAndroidStream);
+                if ((carContext & mSupportedAudioContext) == 0) {
+                    carContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
+                }
+                return carContext;
+            }
+        }
+
+        /**
+         * All updates to external components should be posted to this handler to avoid holding
+         * the internal lock while sending updates.
+         */
+        private final class VolumeHandler extends Handler {
+            @Override
+            public void handleMessage(Message msg) {
+                int stream;
+                int volume;
+                switch (msg.what) {
+                    case MSG_UPDATE_VOLUME:
+                        stream = msg.arg1;
+                        int flag = msg.arg2;
+                        final int size = mVolumeControllers.beginBroadcast();
+                        try {
+                            for (int i = 0; i < size; i++) {
+                                try {
+                                    mVolumeControllers.getBroadcastItem(i)
+                                            .volumeChanged(stream, flag);
+                                } catch (RemoteException ignored) {
+                                }
+                            }
+                        } finally {
+                            mVolumeControllers.finishBroadcast();
+                        }
+                        break;
+                    case MSG_UPDATE_HAL:
+                        stream = msg.arg1;
+                        volume = msg.arg2;
+                        mHal.setStreamVolume(stream, volume);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        public CarExternalVolumeController(Context context, CarAudioService audioService,
+                                           AudioHalService hal, CarInputService inputService) {
+            mContext = context;
+            mAudioService = audioService;
+            mPolicy = audioService.getAudioRoutingPolicy();
+            mHal = hal;
+            mInputService = inputService;
+        }
+
+        @Override
+        void init() {
+            mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts();
+            mHasExternalMemory = mHal.isExternalAudioVolumePersistent();
+            synchronized (this) {
+                initVolumeLimitLocked();
+                initCurrentVolumeLocked();
+            }
+            mInputService.setVolumeKeyListener(this);
+            mHal.setVolumeListener(this);
+            mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this);
+        }
+
+        private void initVolumeLimitLocked() {
+            for (int i : VolumeUtils.LOGICAL_STREAMS) {
+                int carStream = logicalStreamToCarStream(i);
+                Pair<Integer, Integer> volumeMinMax = mHal.getStreamVolumeLimit(carStream);
+                int max;
+                int min;
+                if (volumeMinMax == null) {
+                    max = 0;
+                    min = 0;
+                } else {
+                    max = volumeMinMax.second >= 0 ? volumeMinMax.second : 0;
+                    min = volumeMinMax.first >=0 ? volumeMinMax.first : 0;
+                }
+                // get default stream volume limit first.
+                mLogicalStreamVolumeMax.put(i, max);
+                mLogicalStreamVolumeMin.put(i, min);
+            }
+        }
+
+        private void initCurrentVolumeLocked() {
+            if (mHasExternalMemory) {
+                // TODO: read per context volume from audio hal
+            } else {
+                // TODO: read the Android side volume from Settings and pass it to the audio module
+                // Here we just set it to the physical stream volume temporarily.
+                for (int i : VolumeUtils.LOGICAL_STREAMS) {
+                    mCurrentLogicalVolume.put(i, mHal.getStreamVolume(logicalStreamToCarStream(i)));
+                }
+            }
+        }
+
+        @Override
+        public void setStreamVolume(int stream, int index, int flags) {
+            synchronized (this) {
+                setStreamVolumeInternalLocked(stream, index, flags);
+            }
+        }
+
+        private void setStreamVolumeInternalLocked(int stream, int index, int flags) {
+            if (mLogicalStreamVolumeMax.get(stream) == null) {
+                Log.e(TAG, "Stream type not supported " + stream);
+                return;
+            }
+            int limit = mLogicalStreamVolumeMax.get(stream);
+            if (index > limit) {
+                Log.e(TAG, "Volume exceeds volume limit. stream: " + stream + " index: " + index
+                        + " limit: " + limit);
+                index = limit;
+            }
+
+            if (index < 0) {
+                index = 0;
+            }
+
+            if (mCurrentLogicalVolume.get(stream) == index) {
+                return;
+            }
+
+            int carStream = logicalStreamToCarStream(stream);
+            int carContext = VolumeUtils.androidStreamToCarContext(stream);
+
+            // For single channel, only adjust the volume when the audio context is the current one.
+            if (mCurrentContext == carContext) {
+                mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
+            }
+            // Record the current volume internally.
+            mCurrentLogicalVolume.put(stream, index);
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_VOLUME, stream,
+                    getVolumeUpdateFlag()));
+        }
+
+        @Override
+        public int getStreamVolume(int stream) {
+            synchronized (this) {
+                if (mCurrentLogicalVolume.get(stream) == null) {
+                    Log.d(TAG, "Invalid stream type " + stream);
+                    return 0;
+                }
+                return mCurrentLogicalVolume.get(stream);
+            }
+        }
+
+        @Override
+        public void setVolumeController(IVolumeController controller) {
+            synchronized (this) {
+                mVolumeControllers.register(controller);
+            }
+        }
+
+        @Override
+        public void onVolumeChange(int carStream, int volume, int volumeState) {
+            int flag = getVolumeUpdateFlag();
+            synchronized (this) {
+                // Assume single channel here.
+                int currentLogicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
+                int currentCarStream = logicalStreamToCarStream(currentLogicalStream);
+                if (currentCarStream == carStream) {
+                    mCurrentLogicalVolume.put(currentLogicalStream, volume);
+                    mHandler.sendMessage(
+                            mHandler.obtainMessage(MSG_UPDATE_VOLUME, currentLogicalStream, flag));
+                } else {
+                    // Hal is telling us a car stream volume has changed, but it is not the current
+                    // stream.
+                    Log.w(TAG, "Car stream" + carStream
+                            + " volume changed, but it is not current stream, ignored.");
+                }
+            }
+        }
+
+        private int getVolumeUpdateFlag() {
+            // TODO: Apply appropriate flags.
+            return AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND;
+        }
+
+        private void updateHalVolumeLocked(final int carStream, final int index) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
+        }
+
+        @Override
+        public void onVolumeLimitChange(int streamNumber, int volume) {
+            // TODO: How should this update be sent to SystemUI? maybe send a volume update without
+            // showing UI.
+            synchronized (this) {
+                initVolumeLimitLocked();
+            }
+        }
+
+        @Override
+        public int getStreamMaxVolume(int stream) {
+            synchronized (this) {
+                if (mLogicalStreamVolumeMax.get(stream) == null) {
+                    Log.e(TAG, "Stream type not supported " + stream);
+                    return 0;
+                }
+                return mLogicalStreamVolumeMax.get(stream);
+            }
+        }
+
+        @Override
+        public int getStreamMinVolume(int stream) {
+            synchronized (this) {
+                if (mLogicalStreamVolumeMin.get(stream) == null) {
+                    Log.e(TAG, "Stream type not supported " + stream);
+                    return 0;
+                }
+                return mLogicalStreamVolumeMin.get(stream);
+            }
+        }
+
+        @Override
+        public boolean onKeyEvent(KeyEvent event) {
+            int logicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
+            final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+            // TODO: properly handle long press on volume key
+            if (!down) {
+                return true;
+            }
+
+            synchronized (this) {
+                int currentVolume = mCurrentLogicalVolume.get(logicalStream);
+                switch (event.getKeyCode()) {
+                    case KeyEvent.KEYCODE_VOLUME_UP:
+                        setStreamVolumeInternalLocked(logicalStream, currentVolume + 1,
+                                getVolumeUpdateFlag());
+                        break;
+                    case KeyEvent.KEYCODE_VOLUME_DOWN:
+                        setStreamVolumeInternalLocked(logicalStream, currentVolume - 1,
+                                getVolumeUpdateFlag());
+                        break;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) {
+            synchronized (this) {
+                if (primaryFocusContext == mCurrentContext) {
+                    return;
+                }
+                mCurrentContext = primaryFocusContext;
+
+                int currentVolume = mCurrentLogicalVolume.get(
+                        VolumeUtils.carContextToAndroidStream(primaryFocusContext));
+                if (mSupportedAudioContext == 0) {
+                    // Car does not support audio context, we need to reset the volume
+                    updateHalVolumeLocked(primaryFocusPhysicalStream, currentVolume);
+                } else {
+                    // car supports context, but does not have memory.
+                    if (!mHasExternalMemory) {
+                        updateHalVolumeLocked(primaryFocusContext, currentVolume);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/car/CarVolumeService.java b/service/src/com/android/car/CarVolumeService.java
new file mode 100644
index 0000000..18ab597
--- /dev/null
+++ b/service/src/com/android/car/CarVolumeService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.IVolumeController;
+import android.view.KeyEvent;
+
+import com.android.car.hal.AudioHalService;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioContextFlag;
+
+/**
+ * Handles car volume controls.
+ *
+ * It delegates to a {@link com.android.car.CarVolumeService.CarVolumeController} to do proper
+ * volume controls based on different car properties.
+ *
+ * @hide
+ */
+public class CarVolumeService {
+    private static final String TAG = "CarVolumeService";
+
+    // TODO: need to have a policy to define the default context
+    public static int DEFAULT_CAR_AUDIO_CONTEXT =
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG;
+    private final Context mContext;
+    private final AudioHalService mAudioHal;
+    private final CarAudioService mAudioService;
+    private final CarInputService mInputService;
+
+    private CarVolumeController mCarVolumeController;
+
+    public static int androidStreamToCarUsage(int logicalAndroidStream) {
+        return CarAudioAttributesUtil.getCarUsageFromAudioAttributes(
+                new AudioAttributes.Builder()
+                        .setLegacyStreamType(logicalAndroidStream).build());
+    }
+
+    public CarVolumeService(Context context, CarAudioService audioService, AudioHalService audioHal,
+                            CarInputService inputService) {
+        mContext = context;
+        mAudioHal = audioHal;
+        mAudioService = audioService;
+        mInputService = inputService;
+    }
+
+    public void init() {
+        mCarVolumeController = CarVolumeControllerFactory.createCarVolumeController(mContext,
+                mAudioService, mAudioHal, mInputService);
+
+        mCarVolumeController.init();
+        mInputService.setVolumeKeyListener(mCarVolumeController);
+    }
+
+    public void setStreamVolume(int streamType, int index, int flags) {
+        mCarVolumeController.setStreamVolume(streamType, index, flags);
+    }
+
+    public void setVolumeController(IVolumeController controller) {
+        mCarVolumeController.setVolumeController(controller);
+    }
+
+    public int getStreamMaxVolume(int stream) {
+        return mCarVolumeController.getStreamMaxVolume(stream);
+    }
+
+    public int getStreamMinVolume(int stream) {
+        return mCarVolumeController.getStreamMinVolume(stream);
+    }
+
+    public int getStreamVolume(int stream) {
+        return mCarVolumeController.getStreamVolume(stream);
+    }
+
+    /**
+     * Abstraction layer for volume controls, so that we don't have if-else check for audio
+     * properties everywhere.
+     */
+    public static abstract class CarVolumeController implements CarInputService.KeyEventListener {
+        abstract void init();
+        abstract public void setStreamVolume(int stream, int index, int flags);
+        abstract public void setVolumeController(IVolumeController controller);
+        abstract public int getStreamMaxVolume(int stream);
+        abstract public int getStreamMinVolume(int stream);
+        abstract public int getStreamVolume(int stream);
+    }
+}
diff --git a/service/src/com/android/car/DayNightModePolicy.java b/service/src/com/android/car/DayNightModePolicy.java
index 38a5332..efe007a 100644
--- a/service/src/com/android/car/DayNightModePolicy.java
+++ b/service/src/com/android/car/DayNightModePolicy.java
@@ -16,15 +16,11 @@
 
 package com.android.car;
 
-import android.car.Car;
 import android.car.hardware.CarSensorEvent;
 import android.car.hardware.CarSensorManager;
 import android.content.Context;
-import android.os.SystemClock;
 import android.util.Log;
 
-import com.android.car.hal.SensorHalServiceBase.SensorListener;
-
 import java.io.PrintWriter;
 
 //TODO
@@ -50,7 +46,10 @@
     }
 
     public static CarSensorEvent getDefaultValue(int sensorType) {
-        return createEvent(true /* isNight */);
+        // There's a race condition and timestamp from vehicle HAL could be slightly less
+        // then current call to SystemClock.elapsedRealtimeNanos() will return.
+        // We want vehicle HAL value always override this default value so we set timestamp to 0.
+        return createEvent(true /* isNight */, 0 /* timestamp */);
     }
 
     @Override
@@ -91,9 +90,9 @@
         // TODO Auto-generated method stub
     }
 
-    private static CarSensorEvent createEvent(boolean isNight) {
+    private static CarSensorEvent createEvent(boolean isNight, long timestamp) {
         CarSensorEvent event = new CarSensorEvent(CarSensorManager.SENSOR_TYPE_NIGHT,
-                SystemClock.elapsedRealtimeNanos(), 0, 1);
+                timestamp, 0, 1);
         event.intValues[0] = isNight ? 1 : 0;
         return event;
     }
diff --git a/service/src/com/android/car/DrivingStatePolicy.java b/service/src/com/android/car/DrivingStatePolicy.java
index 2b2402f..f8baba2 100644
--- a/service/src/com/android/car/DrivingStatePolicy.java
+++ b/service/src/com/android/car/DrivingStatePolicy.java
@@ -24,8 +24,6 @@
 import android.os.SystemClock;
 import android.util.Log;
 
-import com.android.car.hal.SensorHalServiceBase.SensorListener;
-
 import java.io.PrintWriter;
 import java.util.List;
 
@@ -97,7 +95,10 @@
                     sensorType);
             return null;
         }
-        return createEvent(CarSensorEvent.DRIVE_STATUS_FULLY_RESTRICTED);
+        // There's a race condition and timestamp from vehicle HAL could be slightly less
+        // then current call to SystemClock.elapsedRealtimeNanos() will return.
+        // We want vehicle HAL value always override this default value so we set timestamp to 0.
+        return createEvent(CarSensorEvent.DRIVE_STATUS_FULLY_RESTRICTED, 0 /* timestamp */);
     }
 
     @Override
@@ -202,8 +203,15 @@
     }
 
     private static CarSensorEvent createEvent(int drivingState) {
-        CarSensorEvent event = new CarSensorEvent(CarSensorManager.SENSOR_TYPE_DRIVING_STATUS,
-                SystemClock.elapsedRealtimeNanos(), 0, 1);
+        return createEvent(drivingState, SystemClock.elapsedRealtimeNanos());
+    }
+
+    private static CarSensorEvent createEvent(int drivingState, long timestamp) {
+        CarSensorEvent event = new CarSensorEvent(
+                CarSensorManager.SENSOR_TYPE_DRIVING_STATUS,
+                timestamp,
+                0 /* float values */,
+                1 /* int values */);
         event.intValues[0] = drivingState;
         return event;
     }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 43061db..87f827c 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -61,6 +61,7 @@
     private final GarageModeService mGarageModeService;
     private final CarNavigationService mCarNavigationService;
     private final InstrumentClusterService mInstrumentClusterService;
+    private final SystemStateControllerService mSystemStateControllerService;
 
     /** Test only service. Populate it only when necessary. */
     @GuardedBy("this")
@@ -93,7 +94,7 @@
         mCarInfoService = new CarInfoService(serviceContext);
         mAppContextService = new AppContextService(serviceContext);
         mCarSensorService = new CarSensorService(serviceContext);
-        mCarAudioService = new CarAudioService(serviceContext);
+        mCarAudioService = new CarAudioService(serviceContext, mCarInputService);
         mCarHvacService = new CarHvacService(serviceContext);
         mCarRadioService = new CarRadioService(serviceContext);
         mCarCameraService = new CarCameraService(serviceContext);
@@ -101,7 +102,9 @@
         mCarPackageManagerService = new CarPackageManagerService(serviceContext);
         mInstrumentClusterService = new InstrumentClusterService(serviceContext);
         mCarNavigationService = new CarNavigationService(
-                serviceContext, mAppContextService, mInstrumentClusterService);
+                mAppContextService, mInstrumentClusterService);
+        mSystemStateControllerService = new SystemStateControllerService(serviceContext,
+                mCarPowerManagementService, this);
 
         // Be careful with order. Service depending on other service should be inited later.
         mAllServices = new CarServiceBase[] {
@@ -120,6 +123,7 @@
                 mInstrumentClusterService,
                 mCarProjectionService,
                 mCarNavigationService,
+                mSystemStateControllerService
                 };
     }
 
diff --git a/service/src/com/android/car/SystemStateControllerService.java b/service/src/com/android/car/SystemStateControllerService.java
new file mode 100644
index 0000000..b9a35e3
--- /dev/null
+++ b/service/src/com/android/car/SystemStateControllerService.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car;
+
+import android.car.media.CarAudioManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.util.Log;
+
+import com.android.car.CarPowerManagementService.PowerEventProcessingHandler;
+import com.android.car.CarPowerManagementService.PowerServiceEventListener;
+
+import java.io.PrintWriter;
+
+public class SystemStateControllerService implements CarServiceBase,
+    PowerServiceEventListener, PowerEventProcessingHandler {
+
+    private final CarPowerManagementService mCarPowerManagementService;
+    private final MediaSilencer mMediaSilencer;
+    private final ICarImpl mICarImpl;
+
+    public SystemStateControllerService(Context context,
+            CarPowerManagementService carPowerManagementSercvice, ICarImpl carImpl) {
+        mCarPowerManagementService = carPowerManagementSercvice;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mMediaSilencer = new MediaSilencer(audioManager);
+        mICarImpl = carImpl;
+    }
+
+    @Override
+    public long onPrepareShutdown(boolean shuttingDown) {
+        //TODO add state saving here for things to restore on power on.
+        return 0;
+    }
+
+    @Override
+    public void onPowerOn(boolean displayOn) {
+        if (displayOn) {
+            Log.i(CarLog.TAG_SYS, "MediaSilencer unmute");
+            if (!mICarImpl.isInMocking()) {
+                mMediaSilencer.unmuteMedia();
+            }
+        } else {
+            Log.i(CarLog.TAG_SYS, "MediaSilencer mute");
+            if (!mICarImpl.isInMocking()) { // do not do this in mocking as it can affect test.
+                mMediaSilencer.muteMedia();
+            }
+        }
+    }
+
+    @Override
+    public int getWakeupTime() {
+        return 0;
+    }
+
+    @Override
+    public void onShutdown() {
+        // TODO
+    }
+
+    @Override
+    public void onSleepEntry() {
+        // TODO
+    }
+
+    @Override
+    public void onSleepExit() {
+        // TODO
+    }
+
+    @Override
+    public void init() {
+        mCarPowerManagementService.registerPowerEventListener(this);
+        mCarPowerManagementService.registerPowerEventProcessingHandler(this);
+    }
+
+    @Override
+    public void release() {
+    }
+
+    @Override
+    public void dump(PrintWriter writer) {
+        writer.println("Media silence, ismuted:" + mMediaSilencer.isMuted());
+    }
+
+    private class MediaSilencer implements AudioManager.OnAudioFocusChangeListener {
+
+        private final AudioManager mAudioManager;
+        private final AudioAttributes mAttribMusic;
+        private boolean mMuteMedia;
+
+        private MediaSilencer(AudioManager audioManager) {
+            mAudioManager = audioManager;
+            mAttribMusic = CarAudioAttributesUtil.getAudioAttributesForCarUsage(
+                    CarAudioManager.CAR_AUDIO_USAGE_MUSIC);
+        }
+
+        private synchronized void muteMedia() {
+            mMuteMedia = true;
+            //TODO replace this into real mute request
+            mAudioManager.requestAudioFocus(this, mAttribMusic, AudioManager.AUDIOFOCUS_GAIN,
+                    AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
+        }
+
+        private synchronized void unmuteMedia() {
+            mMuteMedia = false;
+            mAudioManager.abandonAudioFocus(this);
+        }
+
+        private synchronized boolean isMuted() {
+            return mMuteMedia;
+        }
+
+        @Override
+        public synchronized void onAudioFocusChange(int focusChange) {
+            if (mMuteMedia) {
+                if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
+                        focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
+                        focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+                    Log.w(CarLog.TAG_SYS, "MediaSilencer got audio focus change:" + focusChange);
+                    mAudioManager.requestAudioFocus(this, mAttribMusic,
+                            AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
+                }
+            } else {
+                mAudioManager.abandonAudioFocus(this);
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/car/VolumeUtils.java b/service/src/com/android/car/VolumeUtils.java
new file mode 100644
index 0000000..023d319
--- /dev/null
+++ b/service/src/com/android/car/VolumeUtils.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioContextFlag;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class VolumeUtils {
+    private static final String TAG = "VolumeUtils";
+
+    public static final int[] LOGICAL_STREAMS = {
+            AudioManager.STREAM_VOICE_CALL,
+            AudioManager.STREAM_SYSTEM,
+            AudioManager.STREAM_RING,
+            AudioManager.STREAM_MUSIC,
+            AudioManager.STREAM_ALARM,
+            AudioManager.STREAM_NOTIFICATION,
+            AudioManager.STREAM_DTMF,
+    };
+
+    public static final int[] CAR_AUDIO_CONTEXT = {
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_VOICE_COMMAND_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SAFETY_ALERT_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CD_ROM_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_AUX_AUDIO_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG,
+            VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG
+    };
+
+    public static String streamToName(int stream) {
+        switch (stream) {
+            case AudioManager.STREAM_ALARM: return "Alarm";
+            case AudioManager.STREAM_MUSIC: return "Music";
+            case AudioManager.STREAM_NOTIFICATION: return "Notification";
+            case AudioManager.STREAM_RING: return "Ring";
+            case AudioManager.STREAM_VOICE_CALL: return "Call";
+            case AudioManager.STREAM_SYSTEM: return "System";
+            case AudioManager.STREAM_DTMF: return "DTMF";
+            default: return "Unknown";
+        }
+    }
+
+    public static int androidStreamToCarContext(int logicalAndroidStream) {
+        switch (logicalAndroidStream) {
+            case AudioManager.STREAM_VOICE_CALL:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG;
+            case AudioManager.STREAM_SYSTEM:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
+            case AudioManager.STREAM_RING:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG;
+            case AudioManager.STREAM_MUSIC:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG;
+            case AudioManager.STREAM_ALARM:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG;
+            case AudioManager.STREAM_NOTIFICATION:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG;
+            case AudioManager.STREAM_DTMF:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
+            default:
+                return VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG;
+        }
+    }
+
+    public static int carContextToAndroidStream(int carContext) {
+        switch (carContext) {
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG:
+                return AudioManager.STREAM_VOICE_CALL;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG:
+                return AudioManager.STREAM_SYSTEM;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG:
+                return AudioManager.STREAM_NOTIFICATION;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG:
+                return AudioManager.STREAM_MUSIC;
+            case VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG:
+                return AudioManager.STREAM_ALARM;
+            default:
+                return AudioManager.STREAM_MUSIC;
+        }
+    }
+
+    public static int androidStreamToCarUsage(int logicalAndroidStream) {
+        return CarAudioAttributesUtil.getCarUsageFromAudioAttributes(
+                new AudioAttributes.Builder()
+                        .setLegacyStreamType(logicalAndroidStream).build());
+    }
+
+    private final SparseArray<Float[]> mStreamAmplLookup = new SparseArray<>(7);
+
+    private static final float LN_10 = 2.302585093f;
+    // From cs/#android/frameworks/av/media/libmedia/AudioSystem.cpp
+    private static final float DB_PER_STEP = -.5f;
+
+    private final AudioManager mAudioManager;
+
+    public VolumeUtils(AudioManager audioManager) {
+        mAudioManager = audioManager;
+        for(int i : LOGICAL_STREAMS) {
+            initStreamLookup(i);
+        }
+    }
+
+    private void initStreamLookup(int streamType) {
+        int maxIndex = mAudioManager.getStreamMaxVolume(streamType);
+        Float[] amplList = new Float[maxIndex + 1];
+
+        for (int i = 0; i <= maxIndex; i++) {
+            amplList[i] = volIndexToAmpl(i, maxIndex);
+        }
+        Log.d(TAG, streamToName(streamType) + ": " + Arrays.toString(amplList));
+        mStreamAmplLookup.put(streamType, amplList);
+    }
+
+
+    public static int closestIndex(float desired, Float[] list) {
+        float min = Float.MAX_VALUE;
+        int closestIndex = 0;
+
+        for (int i = 0; i < list.length; i++) {
+            float diff = Math.abs(list[i] - desired);
+            if (diff < min) {
+                min = diff;
+                closestIndex = i;
+            }
+        }
+        return closestIndex;
+    }
+
+    public void adjustStreamVol(int stream, int desired, int actual, int maxIndex) {
+        float gain = getTrackGain(desired, actual, maxIndex);
+        int index = closestIndex(gain, mStreamAmplLookup.get(stream));
+        if (index == mAudioManager.getStreamVolume(stream)) {
+            return;
+        } else {
+            mAudioManager.setStreamVolume(stream, index, 0 /*don't show UI*/);
+        }
+    }
+
+    /**
+     * Returns the gain which, when applied to an a stream with volume
+     * actualVolIndex, will make the output volume equivalent to a stream with a gain of
+     * 1.0 playing on a stream with volume desiredVolIndex.
+     *
+     * Computing this is non-trivial because the gain is applied on a linear scale while the volume
+     * indices map to a log (dB) scale.
+     *
+     * The computation is copied from cs/#android/frameworks/av/media/libmedia/AudioSystem.cpp
+     */
+    float getTrackGain(int desiredVolIndex, int actualVolIndex, int maxIndex) {
+        if (desiredVolIndex == actualVolIndex) {
+            return 1.0f;
+        }
+        return volIndexToAmpl(desiredVolIndex, maxIndex)
+                / volIndexToAmpl(actualVolIndex, maxIndex);
+    }
+
+    /**
+     * Returns the amplitude corresponding to volIndex. Guaranteed to return a non-negative value.
+     */
+    private float volIndexToAmpl(int volIndex, int maxIndex) {
+        // Normalize volIndex to be in the range [0, 100].
+        int volume = (int) ((float) volIndex / maxIndex * 100.0f);
+        return logToLinear(volumeToDecibels(volume));
+    }
+
+    /**
+     * volume is in the range [0, 100].
+     */
+    private static float volumeToDecibels(int volume) {
+        return (100 - volume) * DB_PER_STEP;
+    }
+
+    /**
+     * Corresponds to the function linearToLog in AudioSystem.cpp.
+     */
+    private static float logToLinear(float decibels) {
+        return decibels < 0.0f ? (float) Math.exp(decibels * LN_10 / 20.0f) : 1.0f;
+    }
+}
\ No newline at end of file
diff --git a/service/src/com/android/car/cluster/CarNavigationService.java b/service/src/com/android/car/cluster/CarNavigationService.java
index 311b42a..1e2887d 100644
--- a/service/src/com/android/car/cluster/CarNavigationService.java
+++ b/service/src/com/android/car/cluster/CarNavigationService.java
@@ -21,19 +21,15 @@
 import android.car.navigation.CarNavigationManager;
 import android.car.navigation.ICarNavigation;
 import android.car.navigation.ICarNavigationEventListener;
-import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.Binder;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.car.AppContextService;
 import com.android.car.CarLog;
 import com.android.car.CarServiceBase;
-import com.android.car.cluster.InstrumentClusterService.RendererInitializationListener;
-import com.android.car.cluster.renderer.ThreadSafeNavigationRenderer;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -42,21 +38,18 @@
 /**
  * Service that will push navigation event to navigation renderer in instrument cluster.
  */
-public class CarNavigationService extends ICarNavigation.Stub
-        implements CarServiceBase, RendererInitializationListener {
+public class CarNavigationService extends ICarNavigation.Stub implements CarServiceBase {
     private static final String TAG = CarLog.TAG_NAV;
 
     private final List<CarNavigationEventListener> mListeners = new ArrayList<>();
     private final AppContextService mAppContextService;
-    private final Context mContext;
     private final InstrumentClusterService mInstrumentClusterService;
 
     private volatile CarNavigationInstrumentCluster mInstrumentClusterInfo = null;
     private volatile NavigationRenderer mNavigationRenderer;
 
-    public CarNavigationService(Context context, AppContextService appContextService,
+    public CarNavigationService(AppContextService appContextService,
             InstrumentClusterService instrumentClusterService) {
-        mContext = context;
         mAppContextService = appContextService;
         mInstrumentClusterService = instrumentClusterService;
     }
@@ -64,7 +57,9 @@
     @Override
     public void init() {
         Log.d(TAG, "init");
-        mInstrumentClusterService.registerListener(this);
+        mNavigationRenderer = mInstrumentClusterService.getNavigationRenderer();
+        mInstrumentClusterInfo = mNavigationRenderer != null
+                ? mNavigationRenderer.getNavigationProperties() : null;
     }
 
     @Override
@@ -72,7 +67,6 @@
         synchronized(mListeners) {
             mListeners.clear();
         }
-        mInstrumentClusterService.unregisterListener(this);
     }
 
     @Override
@@ -195,23 +189,6 @@
         return null;
     }
 
-    @Override
-    public void onRendererInitSucceeded() {
-        Log.d(TAG, "onRendererInitSucceeded");
-        mNavigationRenderer = ThreadSafeNavigationRenderer.createFor(
-                Looper.getMainLooper(),
-                mInstrumentClusterService.getNavigationRenderer());
-
-        // TODO: we need to obtain this information from InstrumentClusterRenderer.
-        mInstrumentClusterInfo = CarNavigationInstrumentCluster.createCluster(1000);
-
-        if (isRendererAvailable()) {
-            for (CarNavigationEventListener listener : mListeners) {
-                listener.onInstrumentClusterStart(mInstrumentClusterInfo);
-            }
-        }
-    }
-
     private class CarNavigationEventListener implements IBinder.DeathRecipient {
         final ICarNavigationEventListener listener;
 
@@ -243,7 +220,7 @@
     }
 
     private boolean isRendererAvailable() {
-        boolean available = mNavigationRenderer != null;
+        boolean available = mNavigationRenderer != null && mInstrumentClusterInfo != null;
         if (!available) {
             Log.w(TAG, "Instrument cluster renderer is not available.");
         }
diff --git a/service/src/com/android/car/cluster/InstrumentClusterRendererLoader.java b/service/src/com/android/car/cluster/InstrumentClusterRendererLoader.java
index a986848..6d0a590 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterRendererLoader.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterRendererLoader.java
@@ -20,6 +20,7 @@
 import android.content.ContextWrapper;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.car.CarLog;
@@ -34,7 +35,7 @@
  * library.
  */
 public class InstrumentClusterRendererLoader {
-    private final static String TAG = CarLog.TAG_SERVICE;
+    private final static String TAG = CarLog.TAG_CLUSTER;
 
     private final static String sCreateRendererMethod = "createRenderer";
 
@@ -42,11 +43,18 @@
      * Returns true if instrument cluster renderer installed.
      */
     public static boolean isRendererAvailable(Context context) {
+        String packageName = getRendererPackageName(context);
+        if (TextUtils.isEmpty(packageName)) {
+            Log.d(TAG, "Instrument cluster renderer was not configured.");
+            return false;
+        }
+
         PackageManager packageManager = context.getPackageManager();
         try {
-            packageManager.getPackageInfo(getRendererPackageName(context), 0);
+            packageManager.getPackageInfo(packageName, 0);
             return true;
         } catch (NameNotFoundException e) {
+            Log.e(TAG, "Package not found: " + packageName);
             return false;
         }
     }
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index 1ad9768..47f39fd 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -15,34 +15,17 @@
  */
 package com.android.car.cluster;
 
-import static com.android.car.cluster.InstrumentClusterRendererLoader.createRenderer;
-import static com.android.car.cluster.InstrumentClusterRendererLoader.createRendererPackageContext;
-
-import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.car.cluster.renderer.InstrumentClusterRenderer;
 import android.car.cluster.renderer.NavigationRenderer;
 import android.content.Context;
-import android.hardware.display.DisplayManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.util.Log;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
 
 import com.android.car.CarLog;
 import com.android.car.CarServiceBase;
-import com.android.car.CarServiceUtils;
-import com.android.car.R;
 
 import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * Service responsible for interaction with car's instrument cluster.
@@ -55,192 +38,47 @@
     private static final String TAG = CarLog.TAG_CLUSTER + "."
             + InstrumentClusterService.class.getSimpleName();
 
-    private static final int RENDERER_INIT_TIMEOUT_MS = 10 * 1000;
-    private final static int MSG_TIMEOUT = 1;
-
     private final Context mContext;
-    private final Object mHalSync = new Object();
-    private final CopyOnWriteArrayList<RendererInitializationListener>
-            mRendererInitializationListeners = new CopyOnWriteArrayList<>();
 
     private InstrumentClusterRenderer mRenderer;
 
-    private final Handler mHandler;
-
     public InstrumentClusterService(Context context) {
         mContext = context;
-        mHandler = new TimeoutHandler();
-    }
-
-    public interface RendererInitializationListener {
-        void onRendererInitSucceeded();
     }
 
     @Override
     public void init() {
         Log.d(TAG, "init");
 
-        if (getInstrumentClusterType() == InstrumentClusterType.GRAPHICS) {
-            Display display = getInstrumentClusterDisplay(mContext);
-            boolean rendererFound = InstrumentClusterRendererLoader.isRendererAvailable(mContext);
+        boolean rendererFound = InstrumentClusterRendererLoader.isRendererAvailable(mContext);
 
-            if (display != null && rendererFound) {
-                initRendererOnMainThread(display);
-                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT),
-                        RENDERER_INIT_TIMEOUT_MS);
-            } else {
-                mClusterType = InstrumentClusterType.NONE;
-                Log.w(TAG, "Failed to initialize InstrumentClusterRenderer"
-                        + ", renderer found: " + rendererFound
-                        + ", secondary display: " + (display != null), new RuntimeException());
-
-                return;
-            }
+        if (rendererFound) {
+            mRenderer = InstrumentClusterRendererLoader.createRenderer(mContext);
+            mRenderer.onCreate(mContext);
+            mRenderer.initialize();
+            mRenderer.onStart();
         }
     }
 
-    private void initRendererOnMainThread(final Display display) {
-        CarServiceUtils.runOnMain(new Runnable() {
-            @Override
-            public void run() {
-                Log.d(TAG, "initRendererOnMainThread");
-                try {
-                    InstrumentClusterPresentation presentation =
-                            new InstrumentClusterPresentation(mContext, display);
-
-                    ViewGroup rootView = (ViewGroup) LayoutInflater.from(mContext).inflate(
-                            R.layout.instrument_cluster, null);
-
-                    presentation.setContentView(rootView);
-                    InstrumentClusterRenderer renderer = createRenderer(mContext);
-                    renderer.onCreate(createRendererPackageContext(mContext));
-                    View rendererView = renderer.onCreateView(null);
-                    renderer.initialize();
-                    rootView.addView(rendererView);
-                    presentation.show();
-                    renderer.onStart();
-                    initUiDone(renderer);
-                } catch (Exception e) {
-                    Log.e(TAG, e.getMessage(), e);
-                    throw e;
-                }
-            }
-        });
-    }
-
-    private void initUiDone(final InstrumentClusterRenderer renderer) {
-        Log.d(TAG, "initUiDone");
-        mHandler.removeMessages(MSG_TIMEOUT);
-
-        // Call listeners in service thread.
-        runOnServiceThread(new Runnable() {
-            @Override
-            public void run() {
-                mRenderer = renderer;
-
-                for (RendererInitializationListener listener : mRendererInitializationListeners) {
-                    listener.onRendererInitSucceeded();
-                }
-            }
-        });
-    }
-
     @Override
     public void release() {
         Log.d(TAG, "release");
+        if (mRenderer != null) {
+            mRenderer.onStop();
+            mRenderer = null;
+        }
     }
 
     @Override
     public void dump(PrintWriter writer) {
-        // TODO
-    }
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({
-            InstrumentClusterType.NONE,
-            InstrumentClusterType.METADATA,
-            InstrumentClusterType.GRAPHICS
-    })
-    public @interface InstrumentClusterType {
-        /**
-         * For use privately in this class.
-         * @hide
-         */
-        int UNDEFINED = -1;
-
-        /** Access to instrument cluster is not available */
-        int NONE = 0;
-
-        /** Access to instrument cluster through vehicle HAL using meta-data. */
-        int METADATA = 1;
-
-        /** Access instrument cluster as a secondary display. */
-        int GRAPHICS = 2;
-    }
-
-    @InstrumentClusterType private int mClusterType = InstrumentClusterType.UNDEFINED;
-
-    public boolean isInstrumentClusterAvailable() {
-        return mClusterType != InstrumentClusterType.NONE
-                && mClusterType != InstrumentClusterType.UNDEFINED;
-    }
-
-    public int getInstrumentClusterType() {
-        if (mClusterType == InstrumentClusterType.UNDEFINED) {
-            synchronized (mHalSync) {
-                // TODO: need to pull this information from the HAL
-                mClusterType = getInstrumentClusterDisplay(mContext) != null
-                        ? InstrumentClusterType.GRAPHICS : InstrumentClusterType.NONE;
-            }
-        }
-        return mClusterType;
+        writer.println("**" + getClass().getSimpleName() + "**");
+        writer.println("InstrumentClusterRenderer: " + mRenderer);
+        writer.println("NavigationRenderer: "
+                + (mRenderer != null ? mRenderer.getNavigationRenderer() : null));
     }
 
     @Nullable
     public NavigationRenderer getNavigationRenderer() {
         return mRenderer != null ? mRenderer.getNavigationRenderer() : null;
     }
-
-    public void registerListener(RendererInitializationListener listener) {
-        mRendererInitializationListeners.add(listener);
-    }
-
-    public void unregisterListener(RendererInitializationListener listener) {
-        mRendererInitializationListeners.remove(listener);
-    }
-
-    private static Display getInstrumentClusterDisplay(Context context) {
-        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
-        Display[] displays = displayManager.getDisplays();
-
-        Log.d(TAG, "There are currently " + displays.length + " displays connected.");
-        for (Display display : displays) {
-            Log.d(TAG, "  " + display);
-        }
-
-        if (displays.length > 1) {
-            // TODO: assuming that secondary display is instrument cluster. Put this into settings?
-            return displays[1];
-        }
-        return null;
-    }
-
-    private void runOnServiceThread(final Runnable runnable) {
-        if (Looper.myLooper() == mHandler.getLooper()) {
-            runnable.run();
-        } else {
-            mHandler.post(runnable);
-        }
-    }
-
-    private static class TimeoutHandler extends Handler {
-        @Override
-        public void handleMessage(Message msg) {
-            if (msg.what == MSG_TIMEOUT) {
-                Log.e(TAG, "Renderer initialization timeout.", new RuntimeException());
-            } else {
-                super.handleMessage(msg);
-            }
-        }
-    }
 }
diff --git a/service/src/com/android/car/hal/AudioHalService.java b/service/src/com/android/car/hal/AudioHalService.java
index 893fbff..0e97e4b 100644
--- a/service/src/com/android/car/hal/AudioHalService.java
+++ b/service/src/com/android/car/hal/AudioHalService.java
@@ -15,9 +15,11 @@
  */
 package com.android.car.hal;
 
+import android.car.VehicleZoneUtil;
 import android.car.media.CarAudioManager;
 import android.os.ServiceSpecificException;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.car.AudioRoutingPolicy;
 import com.android.car.CarAudioAttributesUtil;
@@ -33,10 +35,13 @@
 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;
+import com.android.car.vehiclenetwork.VehiclePropValueUtil;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -44,7 +49,6 @@
 import java.util.List;
 
 public class AudioHalService extends HalServiceBase {
-
     public static final int VEHICLE_AUDIO_FOCUS_REQUEST_INVALID = -1;
     public static final int VEHICLE_AUDIO_FOCUS_REQUEST_GAIN =
             VehicleAudioFocusRequest.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN;
@@ -126,7 +130,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 +140,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 +161,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) {
@@ -207,6 +207,44 @@
     }
 
     /**
+     * Returns the volume limits of a stream in the form <min, max>.
+     */
+    public Pair<Integer, Integer> getStreamVolumeLimit(int stream) {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        int supportedContext = getSupportedAudioVolumeContexts();
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        List<Integer> maxs = config.getInt32MaxsList();
+        List<Integer> mins = config.getInt32MinsList();
+
+        if (maxs.size() != mins.size()) {
+            Log.e(CarLog.TAG_AUDIO, "Invalid volume prop config");
+            return null;
+        }
+
+        Pair<Integer, Integer> result = null;
+        if (supportedContext != 0) {
+            int index = VehicleZoneUtil.zoneToIndex(supportedContext, stream);
+            if (index < maxs.size()) {
+                result = new Pair<>(mins.get(index), maxs.get(index));
+            }
+        } else {
+            if (stream < maxs.size()) {
+                result = new Pair<>(mins.get(stream), maxs.get(stream));
+            }
+        }
+
+        if (result == null) {
+            Log.e(CarLog.TAG_AUDIO, "No min/max volume found in vehicle" +
+                    " prop config for stream: " + stream);
+        }
+
+        return result;
+    }
+
+    /**
      * Convert car audio manager stream type (usage) into audio context type.
      */
     public static int logicalStreamToHalContextType(int logicalStream) {
@@ -251,6 +289,37 @@
                 VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, payload);
     }
 
+    public void setStreamVolume(int streamType, int index) {
+        int[] payload = {streamType, index, 0};
+        mVehicleHal.getVehicleNetwork().setIntVectorProperty(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, payload);
+    }
+
+    public int getStreamVolume(int stream) {
+        int[] volume = {stream, 0, 0};
+        VehiclePropValue streamVolume =
+                VehiclePropValueUtil.createIntVectorValue(
+                        VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, volume, 0);
+        VehiclePropValue value = mVehicleHal.getVehicleNetwork().getProperty(streamVolume);
+
+        if (value.getInt32ValuesCount() != 3) {
+            Log.e(CarLog.TAG_AUDIO, "returned value not valid");
+            throw new IllegalStateException("Invalid preset returned from service: "
+                    + value.getInt32ValuesList());
+        }
+
+        int retStreamNum = value.getInt32Values(0);
+        int retVolume = value.getInt32Values(1);
+        int retVolumeState = value.getInt32Values(2);
+
+        if (retStreamNum != stream) {
+            Log.e(CarLog.TAG_AUDIO, "Stream number is not the same: "
+                    + stream + " vs " + retStreamNum);
+            throw new IllegalStateException("Stream number is not the same");
+        }
+        return retVolume;
+    }
+
     public synchronized int getHwVariant() {
         return mVariant;
     }
@@ -262,7 +331,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 +339,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 +447,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 +468,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 +488,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/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 24d5a16..928a8de 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -17,6 +17,9 @@
 package com.android.car.hal;
 
 import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -95,6 +98,8 @@
     private final HashMap<Integer, VehiclePropConfig> mUnclaimedProperties = new HashMap<>();
     private final List<VehiclePropConfig> mAllProperties = new LinkedList<>();
 
+    private final HashMap<Integer, VehiclePropertyEventInfo> mEventLog = new HashMap<>();
+
     private VehicleHal() {
         mHandlerThread = new HandlerThread("VEHICLE-HAL");
         mHandlerThread.start();
@@ -279,6 +284,13 @@
                 HalServiceBase service = mPropertyHandlers.get(v.getProp());
                 service.getDispatchList().add(v);
                 mServicesToDispatch.add(service);
+                VehiclePropertyEventInfo info = mEventLog.get(v.getProp());
+                if (info == null) {
+                    info = new VehiclePropertyEventInfo(v);
+                    mEventLog.put(v.getProp(), info);
+                } else {
+                    info.addNewEvent(v);
+                }
             }
         }
         for (HalServiceBase s : mServicesToDispatch) {
@@ -310,12 +322,12 @@
         writer.println("**All properties**");
         for (VehiclePropConfig config : mAllProperties) {
             StringBuilder builder = new StringBuilder();
-            builder.append("Property:" + Integer.toHexString(config.getProp()));
-            builder.append(",access:" + Integer.toHexString(config.getAccess()));
-            builder.append(",changeMode:" + Integer.toHexString(config.getChangeMode()));
-            builder.append(",valueType:" + Integer.toHexString(config.getValueType()));
-            builder.append(",permission:" + Integer.toHexString(config.getPermissionModel()));
-            builder.append(",config:" + Integer.toHexString(config.getConfigArray(0)));
+            builder.append("Property:0x" + Integer.toHexString(config.getProp()));
+            builder.append(",access:0x" + Integer.toHexString(config.getAccess()));
+            builder.append(",changeMode:0x" + Integer.toHexString(config.getChangeMode()));
+            builder.append(",valueType:0x" + Integer.toHexString(config.getValueType()));
+            builder.append(",permission:0x" + Integer.toHexString(config.getPermissionModel()));
+            builder.append(",config:0x" + Integer.toHexString(config.getConfigArray(0)));
             builder.append(",fs min:" + config.getSampleRateMin());
             builder.append(",fs max:" + config.getSampleRateMax());
             for (int i = 0; i < config.getFloatMaxsCount(); i++) {
@@ -332,5 +344,55 @@
             }
             writer.println(builder.toString());
         }
+        writer.println(String.format("**All Events, now ns:%d**",
+                SystemClock.elapsedRealtimeNanos()));
+        for (VehiclePropertyEventInfo info : mEventLog.values()) {
+            writer.println(String.format("event count:%d, lastEvent:%s",
+                    info.eventCount, dumpVehiclePropValue(info.lastEvent)));
+        }
+    }
+
+    public static String dumpVehiclePropValue(VehiclePropValue value) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Property:0x" + Integer.toHexString(value.getProp()));
+        sb.append(",timestamp:" + value.getTimestamp());
+        sb.append(",value type:0x" + Integer.toHexString(value.getValueType()));
+        sb.append(",zone:0x" + Integer.toHexString(value.getZone()));
+        if (value.getInt32ValuesCount() > 0) {
+            sb.append(",int32 values:");
+            for (int i = 0; i < value.getInt32ValuesCount(); i++) {
+                sb.append("," + value.getInt32Values(i));
+            }
+        }
+        if (value.hasInt64Value()) {
+            sb.append(",int64 value:" + value.getInt64Value());
+        }
+        if (value.getFloatValuesCount() > 0) {
+            sb.append(",float values:");
+            for (int i = 0; i < value.getFloatValuesCount(); i++) {
+                sb.append("," + value.getFloatValues(i));
+            }
+        }
+        if (value.hasStringValue()) {
+            sb.append(",string value:" + value.getStringValue());
+        }
+        if (value.hasBytesValue()) {
+            sb.append(",bytes value:" + value.getBytesValue());
+        }
+        return sb.toString();
+    }
+    private static class VehiclePropertyEventInfo {
+        private int eventCount;
+        private VehiclePropValue lastEvent;
+
+        private VehiclePropertyEventInfo(VehiclePropValue event) {
+            eventCount = 1;
+            lastEvent = event;
+        }
+
+        private void addNewEvent(VehiclePropValue event) {
+            eventCount++;
+            lastEvent = event;
+        }
     }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index a626a86..a7a0013 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -21,11 +21,16 @@
         android:minSdkVersion="22"
         android:targetSdkVersion='23'/>
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
     <uses-permission android:name="android.car.permission.CAR_SPEED" />
+    <uses-permission android:name="android.car.permission.CAR_MILEAGE" />
+    <uses-permission android:name="android.car.permission.CAR_FUEL" />
     <uses-permission android:name="android.car.permission.CAR_HVAC" />
     <uses-permission android:name="android.car.permission.CAR_MOCK_VEHICLE_HAL" />
     <uses-permission android:name="android.car.permission.CAR_CAMERA" />
     <uses-permission android:name="android.car.permission.CAR_NAVIGATION_MANAGER"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <application android:label="@string/app_title"
         android:icon="@drawable/ic_launcher">
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/audio.xml b/tests/EmbeddedKitchenSinkApp/res/layout/audio.xml
index 445a14f..a407c7e 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/audio.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/audio.xml
@@ -170,6 +170,16 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/microphone_off" />
+        <Button
+            android:id="@+id/button_wav_play_start"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/play_wav" />
+        <Button
+            android:id="@+id/button_wav_play_stop"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/stop_wav" />
     </LinearLayout>
 
     <TextView
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/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index aebb27da..a92f608 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -17,7 +17,7 @@
               android:orientation="vertical"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
-              android:layout_marginTop="40dp"
+              android:layout_marginTop="100dp"
               android:layout_marginLeft="40dp">
 
     <LinearLayout
@@ -38,13 +38,5 @@
                     android:text="@string/cluster_stop" android:id="@+id/cluster_stop_button"
                     android:layout_column="0" android:textSize="32sp"/>
         </LinearLayout>
-        <EditText
-                android:layout_width="match_parent"
-                android:layout_height="fill_parent"
-                android:inputType="textMultiLine"
-                android:ems="10"
-                android:id="@+id/cluster_log"
-                android:scrollIndicators="right" android:gravity="top"
-                android:editable="false"/>
     </LinearLayout>
 </LinearLayout>
\ No newline at end of file
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..e5b1ed2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml
@@ -0,0 +1,146 @@
+<?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="70dp"
+            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_radio_focus"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_get_radio_focus" />
+        <Button
+                android:id="@+id/button_release_radio_focus"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_release_radio_focus" />
+    </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" />
+        <Button
+                android:id="@+id/button_radio_get_program_info"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/radio_get_program_info" />
+    </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/layout/sensors.xml b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
new file mode 100644
index 0000000..6ee854a
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
@@ -0,0 +1,36 @@
+<?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" >
+        <TextView
+                android:id="@+id/sensor_info"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/volume_item.xml b/tests/EmbeddedKitchenSinkApp/res/layout/volume_item.xml
new file mode 100644
index 0000000..f62e527
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/volume_item.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2016 Google Inc. All Rights Reserved. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/stream_id"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="20sp"
+        android:layout_weight="2">
+    </TextView>
+    <TextView
+        android:id="@+id/volume_limit"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="10dp"
+        android:gravity="center"
+        android:textSize="20sp"
+        android:layout_weight="2">
+    </TextView>
+    <TextView
+        android:id="@+id/current_volume"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="10dp"
+        android:textSize="20sp"
+        android:layout_weight="2">
+    </TextView>
+    <TextView
+        android:id="@+id/logical_max"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="20dp"
+        android:layout_weight="2"/>
+    <TextView
+        android:id="@+id/logical_volume"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="20dp"
+        android:layout_weight="2"/>
+    <Button
+        android:id="@+id/volume_up"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="20dp"
+        android:text="@string/volume_up_logical" />
+    <Button
+        android:id="@+id/volume_down"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/volume_down_logical" />
+    <Button
+        android:id="@+id/request"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/request" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml
new file mode 100644
index 0000000..238d318
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml
@@ -0,0 +1,53 @@
+<?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:orientation="horizontal" android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="96dp"
+    android:layout_marginLeft="96dp">
+    <ListView
+        android:id="@+id/volume_list"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="3">
+    </ListView>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginLeft="20dp"
+        android:orientation="vertical"
+        android:layout_weight="1">
+
+        <Button
+            android:id="@+id/refresh"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/refresh_volume"/>
+
+        <Button
+            android:id="@+id/volume_up"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/volume_up"/>
+
+        <Button
+            android:id="@+id/volume_down"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/volume_down"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/raw/free_flight.wav b/tests/EmbeddedKitchenSinkApp/res/raw/free_flight.wav
new file mode 100644
index 0000000..a4e14aa
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/raw/free_flight.wav
Binary files differ
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
index e35ee3d..0d23c73 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
@@ -18,4 +18,6 @@
     <dimen name="rvcBtnWidth">150dp</dimen>
     <dimen name="rvcTextSize">10dp</dimen>
     <dimen name="rvcTvHeight">80dp</dimen>
-</resources>
\ No newline at end of file
+    <dimen name="radioInfoMargin">5dp</dimen>
+    <dimen name="sensorInfoMargin">5dp</dimen>
+</resources>
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..a654d54 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -44,6 +44,8 @@
     <string name="play">Play</string>
     <string name="play_pcm_once">Play Once</string>
     <string name="stop">Stop</string>
+    <string name="play_wav">Play WAV</string>
+    <string name="stop_wav">Stop WAV</string>
     <string name="audio_focus">Audio Focus</string>
     <string name="focus_gain">Gain</string>
     <string name="focus_gain_transient_duck">Gain,Transient,Duck</string>
@@ -105,5 +107,62 @@
     <!--  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_radio_focus">Get Radio focus</string>
+    <string name="radio_release_radio_focus">Release Radio focus</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_get_program_info">getProgramInformation</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>
+
+    <!-- sensorts test -->
+    <string name="sensor_na">N/A</string>
+
+    <string name="sensor_environment">Environment[%1$s]: temperature=%2$s, pressure=%3$s</string>
+    <string name="sensor_night">Night[%1$s]: isNight=%2$s</string>
+    <string name="sensor_gear">Gear[%1$s]: gear=%2$s</string>
+    <string name="sensor_parking_break">Parking break[%1$s]: isEngaged=%2$s</string>
+    <string name="sensor_fuel_level">Fuel level[%1$s]: lebel=%2$s, range=%3$s, lowFuelWarning=%4$s</string>
+    <string name="sensor_odometer">Odometer[%1$s]: kms=%2$s</string>
+    <string name="sensor_rpm">Rpm[%1$s]: rpm=%2$s</string>
+    <string name="sensor_speed">Speed[%1$s]: speed=%2$s</string>
+    <string name="sensor_driving_status">Driving status[%1$s]: status=%2$s [bin=%3$s]</string>
+    <string name="sensor_compass">Compass[%1$s]: bear=%2$s, pitch=%3$s, roll=%4$s</string>
+    <string name="sensor_accelerometer">Accelerometer[%1$s]: x=%2$s, y=%3$s, z=%4$s</string>
+    <string name="sensor_gyroscope">Gyroscpoe[%1$s]: x=%2$s, y=%3$s, z=%4$s</string>
+    <string name="sensor_location">Location[%1$s]: lat=%2$s, lon=%3$s, accuracy=%4$s, altitude=%5$s, speed=%6$s, bearing=%7$s</string>
+    <string name="sensor_gps">GPS Satellites[%1$s]: inView: %2$s, inUse: %3$s. %4$s</string>
+    <string name="sensor_single_gps_satellite">(%1$s): usedInFix: %2$s, prn: %3$s, snr: %4$s, azimuth: %5$s, elevation: %6$s</string>
+
+    <string name="volume_test">Volume Test</string>
+    <string name="volume_up_logical">Vol +</string>
+    <string name="volume_down_logical">Vol -</string>
+    <string name="refresh_volume">Refresh volumes</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/CarEmulator.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/CarEmulator.java
index 29aad2c..ad3ac73 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/CarEmulator.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/CarEmulator.java
@@ -20,13 +20,12 @@
 import android.car.test.VehicleHalEmulator.VehicleHalPropertyHandler;
 import android.car.test.VehicleHalEmulator;
 import android.os.SystemClock;
+import android.view.KeyEvent;
 
 import com.android.car.vehiclenetwork.VehicleNetworkConsts;
-import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioExtFocusFlag;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioFocusIndex;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioFocusRequest;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioFocusState;
-import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStream;
 import com.android.car.vehiclenetwork.VehiclePropConfigUtil;
 import com.android.car.vehiclenetwork.VehiclePropValueUtil;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehiclePermissionModel;
@@ -35,6 +34,9 @@
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class CarEmulator {
 
     private final Car mCar;
@@ -44,19 +46,33 @@
             new AudioFocusPropertyHandler();
     private final AudioStreamStatePropertyHandler mAudioStreamStatePropertyHandler =
             new AudioStreamStatePropertyHandler();
+    private final AudioVolumePropertyHandler mAudioVolumePropertyHandler =
+            new AudioVolumePropertyHandler();
+    private final AudioVolumeLimitPropertyHandler mAudioVolumeLimitPropertyHandler =
+            new AudioVolumeLimitPropertyHandler();
+    private static final int STREAM_COUNT = 2;
+    private static final int[] STREAM_MAX_VOLUME = {30, 30};
+    private static final int[] STREAM_MIN_VOLUME = {0, 0};
 
     public CarEmulator(Car car) {
         mCar = car;
         mHalEmulator = new VehicleHalEmulator(car);
+        List<Integer> maxVols = new ArrayList<>();
+        List<Integer> minVols = new ArrayList<>();
+        for(int i = 0; i < STREAM_COUNT; i++) {
+            maxVols.add(STREAM_MAX_VOLUME[i]);
+            minVols.add(STREAM_MIN_VOLUME[i]);
+        }
+
         mHalEmulator.addProperty(
                 VehiclePropConfigUtil.getBuilder(
                         VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS,
                         VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE,
                         VehiclePropChangeMode.VEHICLE_PROP_CHANGE_MODE_ON_CHANGE,
-                        VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC3,
+                        VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC4,
                         VehiclePermissionModel.VEHICLE_PERMISSION_SYSTEM_APP_ONLY,
                         0 /*configFlags*/, 0 /*sampleRateMax*/, 0 /*sampleRateMin*/).build(),
-                        mAudioFocusPropertyHandler);
+                mAudioFocusPropertyHandler);
         mHalEmulator.addProperty(
                 VehiclePropConfigUtil.getBuilder(
                         VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE,
@@ -66,6 +82,34 @@
                         VehiclePermissionModel.VEHICLE_PERMISSION_SYSTEM_APP_ONLY,
                         0 /*configFlags*/, 0 /*sampleRateMax*/, 0 /*sampleRateMin*/).build(),
                         mAudioStreamStatePropertyHandler);
+        mHalEmulator.addProperty(
+                VehiclePropConfigUtil.getBuilder(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME,
+                        VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE,
+                        VehiclePropChangeMode.VEHICLE_PROP_CHANGE_MODE_ON_CHANGE,
+                        VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC3,
+                        VehiclePermissionModel.VEHICLE_PERMISSION_SYSTEM_APP_ONLY,
+                        0 /*configFlags*/, 0 /*sampleRateMax*/, 0 /*sampleRateMin*/,
+                        minVols, maxVols).build(),
+                mAudioVolumePropertyHandler);
+        mHalEmulator.addProperty(
+                VehiclePropConfigUtil.getBuilder(
+                        VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT,
+                        VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE,
+                        VehiclePropChangeMode.VEHICLE_PROP_CHANGE_MODE_ON_CHANGE,
+                        VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC2,
+                        VehiclePermissionModel.VEHICLE_PERMISSION_SYSTEM_APP_ONLY,
+                        0 /*configFlags*/, 0 /*sampleRateMax*/, 0 /*sampleRateMin*/).build(),
+                mAudioVolumeLimitPropertyHandler);
+
+        mHalEmulator.addProperty(
+                VehiclePropConfigUtil.getBuilder(
+                        VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT,
+                        VehiclePropAccess.VEHICLE_PROP_ACCESS_READ,
+                        VehiclePropChangeMode.VEHICLE_PROP_CHANGE_MODE_ON_CHANGE,
+                        VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC4,
+                        VehiclePermissionModel.VEHICLE_PERMISSION_SYSTEM_APP_ONLY,
+                        0 /*configFlags*/, 0 /*sampleRateMax*/, 0 /*sampleRateMin*/).build(),
+                null);
     }
 
     public void start() {
@@ -80,6 +124,14 @@
         mAudioFocusPropertyHandler.setAudioFocusControl(reject);
     }
 
+    public void setStreamVolume(int stream, int volume) {
+        mAudioVolumePropertyHandler.setStreamVolume(stream, volume);
+    }
+
+    public void injectVolumeKey(boolean up) {
+        mAudioVolumePropertyHandler.injectVolumeKey(up);
+    }
+
     private class AudioFocusPropertyHandler implements VehicleHalPropertyHandler {
         private boolean mRejectFocus;
         private int mCurrentFocusState = VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_LOSS;
@@ -97,7 +149,7 @@
                         mCurrentFocusExtState = 0;
                         mRejectFocus = true;
                         int[] values = { mCurrentFocusState, mCurrentFocusStreams,
-                                mCurrentFocusExtState };
+                                mCurrentFocusExtState, 0};
                         injectValue = VehiclePropValueUtil.createIntVectorValue(
                                 VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, values,
                                 SystemClock.elapsedRealtimeNanos());
@@ -108,7 +160,7 @@
                         mCurrentFocusStreams = 0;
                         mCurrentFocusExtState = 0;
                         int[] values = { mCurrentFocusState, mCurrentFocusStreams,
-                                mCurrentFocusExtState };
+                                mCurrentFocusExtState, 0};
                         injectValue = VehiclePropValueUtil.createIntVectorValue(
                                 VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, values,
                                 SystemClock.elapsedRealtimeNanos());
@@ -127,7 +179,7 @@
             synchronized (this) {
                 if (mRejectFocus) {
                     int[] values = { mCurrentFocusState, mCurrentFocusStreams,
-                            mCurrentFocusExtState };
+                            mCurrentFocusExtState, 0};
                     injectValue = VehiclePropValueUtil.createIntVectorValue(
                             VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, values,
                             SystemClock.elapsedRealtimeNanos());
@@ -156,7 +208,7 @@
                     mCurrentFocusStreams = requestedStreams;
                     mCurrentFocusExtState = requestedExtFocus;
                     int[] values = { mCurrentFocusState, mCurrentFocusStreams,
-                            mCurrentFocusExtState };
+                            mCurrentFocusExtState, 0};
                     injectValue = VehiclePropValueUtil.createIntVectorValue(
                             VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, values,
                             SystemClock.elapsedRealtimeNanos());
@@ -170,7 +222,7 @@
         @Override
         public synchronized VehiclePropValue onPropertyGet(VehiclePropValue value) {
             int[] values = { mCurrentFocusState, mCurrentFocusStreams,
-                    mCurrentFocusExtState };
+                    mCurrentFocusExtState, 0};
             return VehiclePropValueUtil.createIntVectorValue(
                     VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS, values,
                     SystemClock.elapsedRealtimeNanos());
@@ -204,4 +256,108 @@
         public void onPropertyUnsubscribe(int property) {
         }
     }
+
+    private static class AudioVolumeLimitPropertyHandler implements VehicleHalPropertyHandler {
+        @Override
+        public void onPropertySet(VehiclePropValue value) {
+        }
+
+        @Override
+        public VehiclePropValue onPropertyGet(VehiclePropValue value) {
+            int stream = value.getInt32Values(
+                    VehicleNetworkConsts.VehicleAudioVolumeLimitIndex
+                            .VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_STREAM);
+
+            int[] values = {stream, 20};
+            VehiclePropValue propValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT, values,
+                    SystemClock.elapsedRealtimeNanos());
+            return propValue;
+        }
+
+        @Override
+        public void onPropertySubscribe(int property, float sampleRate, int zones) {
+        }
+
+        @Override
+        public void onPropertyUnsubscribe(int property) {
+        }
+    }
+
+    private class AudioVolumePropertyHandler implements VehicleHalPropertyHandler {
+        private int[] mStreamVolume = {0, 0};
+
+        public void setStreamVolume(int stream, int index) {
+            if (stream > 1) {
+                return;
+            }
+            mStreamVolume[stream] = index;
+            VehiclePropValue injectValue = null;
+            int[] values = { stream, index, 0 };
+            injectValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, values,
+                    SystemClock.elapsedRealtimeNanos());
+            mHalEmulator.injectEvent(injectValue);
+        }
+
+        public void injectVolumeKey(boolean up) {
+            int[] values = {
+                    VehicleNetworkConsts.VehicleHwKeyInputAction.VEHICLE_HW_KEY_INPUT_ACTION_DOWN,
+                    up ? KeyEvent.KEYCODE_VOLUME_UP : KeyEvent.KEYCODE_VOLUME_DOWN, 0, 0 };
+
+            VehiclePropValue injectValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT, values,
+                    SystemClock.elapsedRealtimeNanos());
+
+            mHalEmulator.injectEvent(injectValue);
+
+            int[] upValues = {
+                    VehicleNetworkConsts.VehicleHwKeyInputAction.VEHICLE_HW_KEY_INPUT_ACTION_UP,
+                    up ? KeyEvent.KEYCODE_VOLUME_UP : KeyEvent.KEYCODE_VOLUME_DOWN, 0, 0 };
+
+            injectValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT, upValues,
+                    SystemClock.elapsedRealtimeNanos());
+
+            mHalEmulator.injectEvent(injectValue);
+        }
+
+        @Override
+        public void onPropertySet(VehiclePropValue value) {
+            int stream = value.getInt32Values(
+                    VehicleNetworkConsts.VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STREAM);
+            int volume = value.getInt32Values(
+                    VehicleNetworkConsts.VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_VOLUME);
+            int state = value.getInt32Values(
+                    VehicleNetworkConsts.VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STATE);
+
+            int[] values = {stream, volume, state};
+            VehiclePropValue injectValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, values,
+                    SystemClock.elapsedRealtimeNanos());
+            mStreamVolume[stream] = volume;
+            mHalEmulator.injectEvent(injectValue);
+        }
+
+        @Override
+        public VehiclePropValue onPropertyGet(VehiclePropValue value) {
+            int stream = value.getInt32Values(
+                    VehicleNetworkConsts.VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STREAM);
+
+            int volume = mStreamVolume[stream];
+            int[] values = {stream, volume, 0};
+            VehiclePropValue propValue = VehiclePropValueUtil.createIntVectorValue(
+                    VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME, values,
+                    SystemClock.elapsedRealtimeNanos());
+            return propValue;
+        }
+
+        @Override
+        public void onPropertySubscribe(int property, float sampleRate, int zones) {
+        }
+
+        @Override
+        public void onPropertyUnsubscribe(int property) {
+        }
+    }
 }
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..c7f15c6 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,9 @@
 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 com.google.android.car.kitchensink.sensor.SensorsTestFragment;
+import com.google.android.car.kitchensink.volume.VolumeTestFragment;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,6 +62,9 @@
     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 static final String MENU_SENSORS = "sensors";
+    private static final String MENU_VOLUME_TEST = "volume test";
 
     private Car mCarApi;
     private CarCameraManager mCameraManager;
@@ -69,12 +75,15 @@
 
 
     private AudioTestFragment mAudioTestFragment;
+    private RadioTestFragment mRadioTestFragment;
+    private SensorsTestFragment mSensorsTestFragment;
     private CameraTestFragment mCameraTestFragment;
     private HvacTestFragment mHvacTestFragment;
     private JobSchedulerFragment mJobFragment;
     private KeyboardFragment mKeyboardFragment;
     private InstrumentClusterFragment mInstrumentClusterFragment;
     private InputTestFragment mInputTestFragment;
+    private VolumeTestFragment mVolumeTestFragment;
 
     private final CarSensorManager.CarSensorEventListener mListener =
             new CarSensorManager.CarSensorEventListener() {
@@ -213,8 +222,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_SENSORS, MENU_VOLUME_TEST, MENU_QUIT
                 };
                 for (String menu : allMenus) {
                     items.add(new CarMenu.Builder(menu).setText(menu).build());
@@ -231,6 +240,16 @@
                     mAudioTestFragment = new AudioTestFragment();
                 }
                 setContentFragment(mAudioTestFragment);
+            } else if (id.equals(MENU_RADIO)) {
+                if (mRadioTestFragment == null) {
+                    mRadioTestFragment = new RadioTestFragment();
+                }
+                setContentFragment(mRadioTestFragment);
+            } else if (id.equals(MENU_SENSORS)) {
+                if (mSensorsTestFragment == null) {
+                    mSensorsTestFragment = new SensorsTestFragment();
+                }
+                setContentFragment(mSensorsTestFragment);
             } else if (id.equals(MENU_CAMERA)) {
                 if (mCameraManager != null) {
                     if (mCameraTestFragment == null) {
@@ -271,6 +290,11 @@
                     mInputTestFragment = new InputTestFragment();
                 }
                 setContentFragment(mInputTestFragment);
+            } else if (id.equals(MENU_VOLUME_TEST)) {
+                if (mVolumeTestFragment == null) {
+                    mVolumeTestFragment = new VolumeTestFragment();
+                }
+                setContentFragment(mVolumeTestFragment);
             } else if (id.equals(MENU_QUIT)) {
                 finish();
             }
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
index 16e2e57..6cd0638 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
@@ -60,6 +60,8 @@
     private Button mMediaPlay;
     private Button mMediaPlayOnce;
     private Button mMediaStop;
+    private Button mWavPlay;
+    private Button mWavStop;
     private Button mNavStart;
     private Button mNavEnd;
     private Button mVrStart;
@@ -78,6 +80,7 @@
     private AudioPlayer mNavGuidancePlayer;
     private AudioPlayer mVrPlayer;
     private AudioPlayer mSystemPlayer;
+    private AudioPlayer mWavPlayer;
     private AudioPlayer[] mAllPlayers;
 
     private Handler mHandler;
@@ -171,12 +174,15 @@
                         mVrAudioAttrib);
                 mSystemPlayer = new AudioPlayer(mContext, R.raw.ring_classic_01,
                         mSystemSoundAudioAttrib);
+                mWavPlayer = new AudioPlayer(mContext, R.raw.free_flight,
+                        mMusicAudioAttrib);
                 mAllPlayers = new AudioPlayer[] {
                         mMusicPlayer,
                         mMusicPlayerShort,
                         mNavGuidancePlayer,
                         mVrPlayer,
-                        mSystemPlayer
+                        mSystemPlayer,
+                        mWavPlayer
                 };
             }
             @Override
@@ -225,6 +231,20 @@
                 mMusicPlayer.stop();
             }
         });
+        mWavPlay = (Button) view.findViewById(R.id.button_wav_play_start);
+        mWavPlay.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mWavPlayer.start(true, true, AudioManager.AUDIOFOCUS_GAIN);
+            }
+        });
+        mWavStop = (Button) view.findViewById(R.id.button_wav_play_stop);
+        mWavStop.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mWavPlayer.stop();
+            }
+        });
         mNavPlayOnce = (Button) view.findViewById(R.id.button_nav_play_once);
         mNavPlayOnce.setOnClickListener(new OnClickListener() {
             @Override
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index 91f8d39..421bb42 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -27,7 +27,6 @@
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 
 import com.google.android.car.kitchensink.R;
@@ -53,20 +52,10 @@
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
             @Nullable Bundle savedInstanceState) {
-        View view = inflater.inflate(R.layout.instrument_cluster, container);
+        View view = inflater.inflate(R.layout.instrument_cluster, container, false);
 
-        view.findViewById(R.id.cluster_start_button).setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                initCluster();
-            }
-        });
-        view.findViewById(R.id.cluster_turn_left_button).setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                turnLeft();
-            }
-        });
+        view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
+        view.findViewById(R.id.cluster_turn_left_button).setOnClickListener(v -> turnLeft());
 
         return super.onCreateView(inflater, container, savedInstanceState);
     }
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..3acb5cf 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,57 @@
         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) {
+            try {
+                mSensorManager.unregisterListener(mCarSensorListener);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Car not connected", e);
+            }
+        }
+    }
+
+    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..b127e2b
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java
@@ -0,0 +1,464 @@
+/*
+ * 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.CarNotConnectedException;
+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 mGetRadioFocus;
+    private Button mReleaseRadioFocus;
+    private Button mGetFocus;
+    private Button mReleaseFocus;
+    private Button mRadioNext;
+    private Button mRadioPrev;
+    private Button mRadioScanCancel;
+    private Button mRadioGetProgramInfo;
+    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 mHasRadioFocus;
+    private boolean mHasSecondaryFocus;
+    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);
+        mGetRadioFocus = (Button) view.findViewById(R.id.button_get_radio_focus);
+        mReleaseRadioFocus = (Button) view.findViewById(R.id.button_release_radio_focus);
+        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);
+        mRadioGetProgramInfo = (Button) view.findViewById(R.id.button_radio_get_program_info);
+        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) {
+                try {
+                    mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+                    mRadioAudioAttrib = mCarAudioManager.getAudioAttributesForCarUsage(
+                            CarAudioManager.CAR_AUDIO_USAGE_RADIO);
+                } catch (CarNotConnectedException e) {
+                    Log.e(TAG, "Car not connected", e);
+                }
+            }
+
+            @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();
+            }
+        });
+        mGetRadioFocus.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Get radio focus");
+                }
+                mCarAudioManager.requestAudioFocus(mRadioFocusListener, mRadioAudioAttrib,
+                        AudioManager.AUDIOFOCUS_GAIN, 0);
+                mHasRadioFocus = true;
+                updateStates();
+            }
+        });
+        mReleaseRadioFocus.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Release radio focus");
+                }
+                mCarAudioManager.abandonAudioFocus(mRadioFocusListener, mRadioAudioAttrib);
+                mHasRadioFocus = false;
+                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);
+                mHasSecondaryFocus = true;
+                updateStates();
+            }
+        });
+        mReleaseFocus.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "Release secondary focus");
+                }
+                mAudioManager.abandonAudioFocus(mSecondaryFocusListener);
+                mHasSecondaryFocus = 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();
+            }
+        });
+        mRadioGetProgramInfo.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (DBG) {
+                    Log.i(TAG, "getProgramInformation");
+                }
+                if (mRadioTuner != null) {
+                    RadioManager.ProgramInfo[] programInfos = new RadioManager.ProgramInfo[1];
+                    mRadioTuner.getProgramInformation(programInfos);
+                    addLog(Log.INFO, "mRadioTuner.getProgramInformation() =>" + programInfos[0]);
+                }
+            }
+        });
+        mRadioBand.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                if (DBG) {
+                    Log.i(TAG, "Changing radio band");
+                }
+                if (mRadioTuner != null) {
+                    mRadioTuner.setConfiguration(mRadioBand.isChecked() ? mFmConfig : mAmConfig);
+                }
+                resetMessages();
+                updateMessages();
+                updateStates();
+            }
+        });
+    }
+
+    private void updateStates() {
+        mOpenRadio.setEnabled(mRadioTuner == null);
+        mCloseRadio.setEnabled(mRadioTuner != null);
+        mGetRadioFocus.setEnabled(!mHasRadioFocus);
+        mReleaseRadioFocus.setEnabled(mHasRadioFocus);
+        mGetFocus.setEnabled(!mHasSecondaryFocus);
+        mReleaseFocus.setEnabled(mHasSecondaryFocus);
+        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");
+        }
+        if (mRadioTuner != null) {
+            Log.w(TAG, "Radio tuner already open");
+            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");
+        }
+        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/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
new file mode 100644
index 0000000..468ac39
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
@@ -0,0 +1,384 @@
+/*
+ * 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.sensor;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.car.Car;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.car.CarNotConnectedException;
+import android.support.car.CarNotSupportedException;
+import android.support.car.app.menu.CarDrawerActivity;
+import android.support.car.hardware.CarSensorEvent;
+import android.support.car.hardware.CarSensorManager;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.android.car.kitchensink.R;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class SensorsTestFragment extends Fragment {
+    private static final String TAG = "CAR.SENSOR.KS";
+    private static final boolean DBG = true;
+    private static final boolean DBG_VERBOSE = false;
+    private static final int KS_PERMISSIONS_REQUEST = 1;
+
+    private final static String[] REQUIRED_PERMISSIONS = new String[]{
+        Manifest.permission.ACCESS_FINE_LOCATION,
+        Manifest.permission.ACCESS_COARSE_LOCATION,
+        Car.PERMISSION_MILEAGE,
+        Car.PERMISSION_FUEL,
+        Car.PERMISSION_SPEED
+    };
+
+    private final CarSensorManager.CarSensorEventListener mCarSensorListener =
+            new CarSensorManager.CarSensorEventListener() {
+                @Override
+                public void onSensorChanged(CarSensorEvent event) {
+                    if (DBG_VERBOSE) {
+                        Log.v(TAG, "New car sensor event: " + event);
+                    }
+                    synchronized (SensorsTestFragment.this) {
+                        mEventMap.put(event.sensorType, event);
+                    }
+                    refreshUi();
+                }
+            };
+    private final Handler mHandler = new Handler();
+    private final Map<Integer, CarSensorEvent> mEventMap = new ConcurrentHashMap<>();
+    private final DateFormat mDateFormat = SimpleDateFormat.getDateTimeInstance();
+
+    private CarDrawerActivity mActivity;
+    private TextView mSensorInfo;
+    private Car mCar;
+    private CarSensorManager mSensorManager;
+    private String mNaString;
+    private int[] supportedSensors = new int[0];
+    private Set<String> mActivePermissions = new HashSet<String>();
+
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        if (DBG) {
+            Log.i(TAG, "onCreateView");
+        }
+
+        View view = inflater.inflate(R.layout.sensors, container, false);
+        mActivity = (CarDrawerActivity) getHost();
+
+        mSensorInfo = (TextView) view.findViewById(R.id.sensor_info);
+        mNaString = getContext().getString(R.string.sensor_na);
+
+        return view;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        initPermissions();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mSensorManager != null) {
+            try {
+                mSensorManager.unregisterListener(mCarSensorListener);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Car not connected", e);
+            }
+        }
+    }
+
+    private void initSensors() {
+        try {
+            mSensorManager = (CarSensorManager)
+                    mActivity.getCar().getCarManager(Car.SENSOR_SERVICE);
+            supportedSensors = mSensorManager.getSupportedSensors();
+            for (Integer sensor : supportedSensors) {
+                if ((sensor == CarSensorManager.SENSOR_TYPE_LOCATION
+                     || sensor == CarSensorManager.SENSOR_TYPE_GPS_SATELLITE)
+                    && !mActivePermissions.contains(Manifest.permission.ACCESS_FINE_LOCATION)) {
+                    continue;
+                }
+                mSensorManager.registerListener(mCarSensorListener, sensor,
+                        CarSensorManager.SENSOR_RATE_NORMAL);
+            }
+        } catch (CarNotSupportedException | CarNotConnectedException e) {
+            Log.e(TAG, "Car not connected or not supported", e);
+        }
+    }
+
+    private void initPermissions() {
+        Set<String> missingPermissions = checkExistingPermissions();
+        if (!missingPermissions.isEmpty()) {
+            requestPermissions(missingPermissions);
+        } else {
+            initSensors();
+        }
+    }
+
+    private Set<String> checkExistingPermissions() {
+        Set<String> missingPermissions = new HashSet<String>();
+        for (String permission : REQUIRED_PERMISSIONS) {
+            if (mActivity.getContext().checkSelfPermission(permission)
+                == PackageManager.PERMISSION_GRANTED) {
+                mActivePermissions.add(permission);
+            } else {
+                missingPermissions.add(permission);
+            }
+        }
+        return missingPermissions;
+    }
+
+    private void requestPermissions(Set<String> permissions) {
+        Log.d(TAG, "requesting additional permissions=" + permissions);
+
+        requestPermissions(permissions.toArray(new String[permissions.size()]),
+                KS_PERMISSIONS_REQUEST);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        Log.d(TAG, "onRequestPermissionsResult reqCode=" + requestCode);
+        if (KS_PERMISSIONS_REQUEST == requestCode) {
+            for (int i=0; i<permissions.length; i++) {
+                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+                    mActivePermissions.add(permissions[i]);
+                }
+            }
+            initSensors();
+        }
+    }
+
+
+
+    private void refreshUi() {
+        String summaryString;
+        synchronized (this) {
+            List<String> summary = new ArrayList<>();
+            for (Integer i : supportedSensors) {
+                CarSensorEvent event = mEventMap.get(i);
+                switch (i) {
+                    case CarSensorManager.SENSOR_TYPE_COMPASS:
+                        summary.add(getCompassString(event));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_CAR_SPEED:
+                        summary.add(getContext().getString(R.string.sensor_speed,
+                                getTimestamp(event),
+                                event == null ? mNaString : event.getCarSpeedData(null).carSpeed));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_RPM:
+                        summary.add(getContext().getString(R.string.sensor_rpm,
+                                getTimestamp(event),
+                                event == null ? mNaString : event.getRpmData(null).rpm));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_ODOMETER:
+                        summary.add(getContext().getString(R.string.sensor_odometer,
+                                getTimestamp(event),
+                                event == null ? mNaString : event.getOdometerData(null).kms));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_FUEL_LEVEL:
+                        String level = mNaString;
+                        String range = mNaString;
+                        String lowFuelWarning = mNaString;
+                        if (event != null) {
+                            CarSensorEvent.FuelLevelData fuelData = event.getFuelLevelData(null);
+                            level = fuelData.level == -1 ? level : String.valueOf(fuelData.level);
+                            range = fuelData.range == -1 ? range : String.valueOf(fuelData.range);
+                            lowFuelWarning = String.valueOf(fuelData.lowFuelWarning);
+                        }
+                        summary.add(getContext().getString(R.string.sensor_fuel_level,
+                                getTimestamp(event), level, range, lowFuelWarning));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_PARKING_BRAKE:
+                        summary.add(getContext().getString(R.string.sensor_parking_break,
+                                getTimestamp(event),
+                                event == null ? mNaString :
+                                event.getParkingBrakeData(null).isEngaged));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_GEAR:
+                        summary.add(getContext().getString(R.string.sensor_gear,
+                                getTimestamp(event),
+                                event == null ? mNaString : event.getGearData(null).gear));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_NIGHT:
+                        summary.add(getContext().getString(R.string.sensor_night,
+                                getTimestamp(event),
+                                event == null ? mNaString : event.getNightData(null).isNightMode));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_LOCATION:
+                        summary.add(getLocationString(event));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_DRIVING_STATUS:
+                        String drivingStatus = mNaString;
+                        String binDrivingStatus = mNaString;
+                        if (event != null) {
+                            CarSensorEvent.DrivingStatusData drivingStatusData =
+                                    event.getDrivingStatusData(null);
+                            drivingStatus = String.valueOf(drivingStatusData.status);
+                            binDrivingStatus = Integer.toBinaryString(drivingStatusData.status);
+                        }
+                        summary.add(getContext().getString(R.string.sensor_driving_status,
+                                getTimestamp(event), drivingStatus, binDrivingStatus));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_ENVIRONMENT:
+                        String temperature = mNaString;
+                        String pressure = mNaString;
+                        if (event != null) {
+                            CarSensorEvent.EnvironmentData env = event.getEnvironmentData(null);
+                            temperature = Float.isNaN(env.temperature) ? temperature :
+                                    String.valueOf(env.temperature);
+                            pressure = Float.isNaN(env.pressure) ? pressure :
+                                    String.valueOf(env.pressure);
+                        }
+                        summary.add(getContext().getString(R.string.sensor_environment,
+                                getTimestamp(event), temperature, pressure));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_ACCELEROMETER:
+                        summary.add(getAccelerometerString(event));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_GPS_SATELLITE:
+                        summary.add(getGpsSatteliteString(event));
+                        break;
+                    case CarSensorManager.SENSOR_TYPE_GYROSCOPE:
+                        summary.add(getGyroscopeString(event));
+                    default:
+                        // Should never happen.
+                        Log.w(TAG, "Unrecognized event type: " + i);
+                }
+            }
+            summaryString = TextUtils.join("\n", summary);
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mSensorInfo.setText(summaryString);
+            }
+        });
+    }
+
+    private String getTimestamp(CarSensorEvent event) {
+        if (event == null) {
+            return mNaString;
+        }
+        return mDateFormat.format(new Date(event.timeStampNs / 1000L));
+    }
+
+    private String getCompassString(CarSensorEvent event) {
+        String bear = mNaString;
+        String pitch = mNaString;
+        String roll = mNaString;
+        if (event != null) {
+            CarSensorEvent.CompassData compass = event.getCompassData(null);
+            bear = Float.isNaN(compass.bearing) ? bear : String.valueOf(compass.bearing);
+            pitch = Float.isNaN(compass.pitch) ? pitch : String.valueOf(compass.pitch);
+            roll = Float.isNaN(compass.roll) ? roll : String.valueOf(compass.roll);
+        }
+        return getContext().getString(R.string.sensor_compass,
+                getTimestamp(event), bear, pitch, roll);
+    }
+
+    private String getGyroscopeString(CarSensorEvent event) {
+        String x = mNaString;
+        String y = mNaString;
+        String z = mNaString;
+        if (event != null) {
+            CarSensorEvent.GyroscopeData gyro = event.getGyroscopeData(null);
+            x = Float.isNaN(gyro.x) ? x : String.valueOf(gyro.x);
+            y = Float.isNaN(gyro.y) ? y : String.valueOf(gyro.y);
+            z = Float.isNaN(gyro.z) ? z : String.valueOf(gyro.z);
+        }
+        return getContext().getString(R.string.sensor_gyroscope,
+                getTimestamp(event), x, y, z);
+    }
+
+    private String getAccelerometerString(CarSensorEvent event) {
+        String x = mNaString;
+        String y = mNaString;
+        String z = mNaString;
+        if (event != null) {
+            CarSensorEvent.AccelerometerData gyro = event.getAccelerometerData(null);
+            x = Float.isNaN(gyro.x) ? x : String.valueOf(gyro.x);
+            y = Float.isNaN(gyro.y) ? y : String.valueOf(gyro.y);
+            z = Float.isNaN(gyro.z) ? z : String.valueOf(gyro.z);
+        }
+        return getContext().getString(R.string.sensor_accelerometer,
+                getTimestamp(event), x, y, z);
+    }
+
+    private String getLocationString(CarSensorEvent event) {
+        String lat = mNaString;
+        String lon = mNaString;
+        String accuracy = mNaString;
+        String alt = mNaString;
+        String speed = mNaString;
+        String bearing = mNaString;
+        if (event != null) {
+            Location location = event.getLocation(null);
+            lat = String.valueOf(location.getLatitude());
+            lon = String.valueOf(location.getLongitude());
+            accuracy = location.hasAccuracy() ? String.valueOf(location.getAccuracy()) : accuracy;
+            alt = location.hasAltitude() ? String.valueOf(location.getAltitude()) : alt;
+            speed = location.hasSpeed() ? String.valueOf(location.getSpeed()) : speed;
+            bearing = location.hasBearing() ? String.valueOf(location.getBearing()) : bearing;
+        }
+        return getContext().getString(R.string.sensor_location,
+                getTimestamp(event), lat, lon, accuracy, alt, speed, bearing);
+    }
+
+    private String getGpsSatteliteString(CarSensorEvent event) {
+        String inUse = mNaString;
+        String inView = mNaString;
+        String perSattelite = "";
+        if (event != null) {
+            CarSensorEvent.GpsSatelliteData gpsData = event.getGpsSatelliteData(null, true);
+            inUse = gpsData.numberInUse != -1 ? String.valueOf(gpsData.numberInUse) : inUse;
+            inView = gpsData.numberInView != -1 ? String.valueOf(gpsData.numberInView) : inView;
+            List<String> perSatteliteList = new ArrayList<>();
+            int num = gpsData.usedInFix.length;
+            for (int i=0; i<num; i++) {
+                perSatteliteList.add(getContext().getString(R.string.sensor_single_gps_satellite,
+                        i+1, gpsData.usedInFix[i], gpsData.prn[i], gpsData.snr[i],
+                        gpsData.azimuth[i], gpsData.elevation[i]));
+            }
+            perSattelite = TextUtils.join(", ", perSatteliteList);
+        }
+        return getContext().getString(R.string.sensor_gps,
+                getTimestamp(event), inView, inUse, perSattelite);
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java
new file mode 100644
index 0000000..c66d242
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.car.kitchensink.volume;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.google.android.car.kitchensink.R;
+import com.google.android.car.kitchensink.volume.VolumeTestFragment.VolumeInfo;
+
+
+public class VolumeAdapter extends ArrayAdapter<VolumeInfo> {
+
+    private final Context mContext;
+    private VolumeInfo[] mVolumeList;
+    private final int mLayoutResourceId;
+    private VolumeTestFragment mFragment;
+
+
+    public VolumeAdapter(Context c, int layoutResourceId, VolumeInfo[] locations,
+            VolumeTestFragment fragment) {
+        super(c, layoutResourceId, locations);
+        mFragment = fragment;
+        mContext = c;
+        this.mLayoutResourceId = layoutResourceId;
+        this.mVolumeList = locations;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        ViewHolder vh = new ViewHolder();
+        if (convertView == null) {
+            LayoutInflater inflater = LayoutInflater.from(mContext);
+            convertView = inflater.inflate(mLayoutResourceId, parent, false);
+            vh.id = (TextView) convertView.findViewById(R.id.stream_id);
+            vh.maxVolume = (TextView) convertView.findViewById(R.id.volume_limit);
+            vh.currentVolume = (TextView) convertView.findViewById(R.id.current_volume);
+            vh.logicalVolume = (TextView) convertView.findViewById(R.id.logical_volume);
+            vh.logicalMax = (TextView) convertView.findViewById(R.id.logical_max);
+            vh.upButton = (Button) convertView.findViewById(R.id.volume_up);
+            vh.downButton = (Button) convertView.findViewById(R.id.volume_down);
+            vh.requestButton = (Button) convertView.findViewById(R.id.request);
+            convertView.setTag(vh);
+        } else {
+            vh = (ViewHolder) convertView.getTag();
+        }
+        if (mVolumeList[position] != null) {
+            vh.id.setText(mVolumeList[position].mId);
+            vh.maxVolume.setText(String.valueOf(mVolumeList[position].mMax));
+            vh.currentVolume.setText(String.valueOf(mVolumeList[position].mCurrent));
+            vh.logicalVolume.setText(String.valueOf(mVolumeList[position].mLogicalCurrent));
+            vh.logicalMax.setText(String.valueOf(mVolumeList[position].mLogicalMax));
+            int color = mVolumeList[position].mHasFocus ? Color.GREEN : Color.GRAY;
+            vh.requestButton.setBackgroundColor(color);
+            if (position == 0) {
+                vh.upButton.setVisibility(View.INVISIBLE);
+                vh.downButton.setVisibility(View.INVISIBLE);
+                vh.requestButton.setVisibility(View.INVISIBLE);
+            } else {
+                vh.upButton.setVisibility(View.VISIBLE);
+                vh.downButton.setVisibility(View.VISIBLE);
+                vh.requestButton.setVisibility(View.VISIBLE);
+                vh.upButton.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        mFragment.adjustVolumeByOne(mVolumeList[position].logicalStream, true);
+                    }
+                });
+
+                vh.downButton.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        mFragment.adjustVolumeByOne(mVolumeList[position].logicalStream, false);
+                    }
+                });
+
+                vh.requestButton.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        mFragment.requestFocus(mVolumeList[position].logicalStream);
+                    }
+                });
+            }
+        }
+        return convertView;
+    }
+
+    @Override
+    public int getCount() {
+        return mVolumeList.length;
+    }
+
+
+    public void refreshVolumes(VolumeInfo[] volumes) {
+        mVolumeList = volumes;
+        notifyDataSetChanged();
+    }
+
+    static class ViewHolder {
+        TextView id;
+        TextView maxVolume;
+        TextView currentVolume;
+        TextView logicalMax;
+        TextView logicalVolume;
+        Button upButton;
+        Button downButton;
+        Button requestButton;
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
new file mode 100644
index 0000000..4655f3c
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.car.kitchensink.volume;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.media.CarAudioManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.IVolumeController;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ListView;
+
+import com.google.android.car.kitchensink.CarEmulator;
+import com.google.android.car.kitchensink.R;
+
+public class VolumeTestFragment extends Fragment{
+    private static final String TAG = "CarVolumeTest";
+    private static final int MSG_VOLUME_CHANGED = 0;
+    private static final int MSG_REQUEST_FOCUS = 1;
+    private static final int MSG_FOCUS_CHANGED= 2;
+
+    private ListView mVolumeList;
+    private Button mRefreshButton;
+    private AudioManager mAudioManager;
+    private VolumeAdapter mAdapter;
+
+    private CarAudioManager mCarAudioManager;
+    private Car mCar;
+    private CarEmulator mCarEmulator;
+
+    private Button mVolumeUp;
+    private Button mVolumeDown;
+
+    private final VolumeController mVolumeController = new VolumeController();
+    private final Handler mHandler = new VolumeHandler();
+
+    private class VolumeHandler extends Handler {
+        private AudioFocusListener mFocusListener;
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_VOLUME_CHANGED:
+                    initVolumeInfo();
+                    break;
+                case MSG_REQUEST_FOCUS:
+                    int stream = msg.arg1;
+                    if (mFocusListener != null) {
+                        mAudioManager.abandonAudioFocus(mFocusListener);
+                        mVolumeInfos[mStreamIndexMap.get(stream)].mHasFocus = false;
+                        mAdapter.notifyDataSetChanged();
+                    }
+
+                    mFocusListener = new AudioFocusListener(stream);
+                    mAudioManager.requestAudioFocus(mFocusListener, stream,
+                            AudioManager.AUDIOFOCUS_GAIN);
+                    break;
+                case MSG_FOCUS_CHANGED:
+                    int focusStream = msg.arg1;
+                    mVolumeInfos[mStreamIndexMap.get(focusStream)].mHasFocus = true;
+                    mAdapter.refreshVolumes(mVolumeInfos);
+                    break;
+
+            }
+        }
+    }
+
+    private VolumeInfo[] mVolumeInfos = new VolumeInfo[LOGICAL_STREAMS.length + 1];
+    private SparseIntArray mStreamIndexMap = new SparseIntArray(LOGICAL_STREAMS.length);
+
+    private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
+        private final int mStream;
+        public AudioFocusListener(int stream) {
+            mStream = stream;
+        }
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                mHandler.sendMessage(mHandler.obtainMessage(MSG_FOCUS_CHANGED, mStream, 0));
+            } else {
+                Log.e(TAG, "Audio focus request failed");
+            }
+        }
+    }
+
+    public static class VolumeInfo {
+        public int logicalStream;
+        public String mId;
+        public String mMax;
+        public String mCurrent;
+        public String mLogicalMax;
+        public String mLogicalCurrent;
+        public boolean mHasFocus;
+    }
+
+    private static final int LOGICAL_STREAMS[] = {
+            AudioManager.STREAM_MUSIC,
+            AudioManager.STREAM_ALARM,
+            AudioManager.STREAM_NOTIFICATION,
+            AudioManager.STREAM_RING,
+            AudioManager.STREAM_VOICE_CALL,
+            AudioManager.STREAM_SYSTEM
+            //            AudioManager.STREAM_DTMF,
+    };
+
+    private static String streamToName (int stream) {
+        switch (stream) {
+            case AudioManager.STREAM_ALARM: return "Alarm";
+            case AudioManager.STREAM_MUSIC: return "Music";
+            case AudioManager.STREAM_NOTIFICATION: return "Notification";
+            case AudioManager.STREAM_RING: return "Ring";
+            case AudioManager.STREAM_VOICE_CALL: return "Call";
+            case AudioManager.STREAM_SYSTEM: return "System";
+            default: return "Unknown";
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.volume_test, container, false);
+
+        mVolumeList = (ListView) v.findViewById(R.id.volume_list);
+        mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+
+        mRefreshButton = (Button) v.findViewById(R.id.refresh);
+        mAdapter = new VolumeAdapter(getContext(), R.layout.volume_item, mVolumeInfos, this);
+        mVolumeList.setAdapter(mAdapter);
+
+        mRefreshButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                initVolumeInfo();
+            }
+        });
+
+        mCar = Car.createCar(getContext(), new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mCarEmulator = new CarEmulator(mCar);
+                mCarEmulator.start();
+                try {
+                    mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+                    initVolumeInfo();
+                    mCarAudioManager.setVolumeController(mVolumeController);
+                } catch (CarNotConnectedException e) {
+                    Log.e(TAG, "Car not connected!");
+
+                }
+            }
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+            }
+        }, Looper.getMainLooper());
+        mCar.connect();
+        mVolumeUp = (Button) v.findViewById(R.id.volume_up);
+        mVolumeDown = (Button) v.findViewById(R.id.volume_down);
+
+        mVolumeUp.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                mCarEmulator.injectVolumeKey(true);
+            }
+        });
+
+        mVolumeDown.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                mCarEmulator.injectVolumeKey(false);
+            }
+        });
+
+        return v;
+    }
+
+    public void adjustVolumeByOne(int logicalStream, boolean up) {
+        if (mCarAudioManager == null) {
+            Log.e(TAG, "CarAudioManager is null");
+            return;
+        }
+        int current = 0;
+        try {
+            current = mCarAudioManager.getStreamVolume(logicalStream);
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "car not connected", e);
+        }
+        setStreamVolume(logicalStream, current + (up ? 1 : -1));
+    }
+
+    public void requestFocus(int logicalStream) {
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_REQUEST_FOCUS, logicalStream));
+    }
+
+    public void setStreamVolume(int logicalStream, int volume) {
+        if (mCarAudioManager == null) {
+            Log.e(TAG, "CarAudioManager is null");
+            return;
+        }
+        try {
+            mCarAudioManager.setStreamVolume(logicalStream, volume, 0);
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "car not connected", e);
+        }
+
+        Log.d(TAG, "Set stream " + logicalStream + " volume " + volume);
+    }
+
+
+    private void initVolumeInfo() {
+        if (mVolumeInfos[0] == null) {
+            mVolumeInfos[0] = new VolumeInfo();
+            mVolumeInfos[0].mId = "Stream";
+            mVolumeInfos[0].mCurrent = "Current";
+            mVolumeInfos[0].mMax = "Max";
+            mVolumeInfos[0].mLogicalMax = "Android_Max";
+            mVolumeInfos[0].mLogicalCurrent = "Android_Current";
+
+        }
+        int i = 1;
+        for (int stream : LOGICAL_STREAMS) {
+            if (mVolumeInfos[i] == null) {
+                mVolumeInfos[i] = new VolumeInfo();
+            }
+            mVolumeInfos[i].logicalStream = stream;
+            mStreamIndexMap.put(stream, i);
+            mVolumeInfos[i].mId = streamToName(stream);
+
+            int current = 0;
+            int max = 0;
+            try {
+                current = mCarAudioManager.getStreamVolume(stream);
+                max = mCarAudioManager.getStreamMaxVolume(stream);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "car not connected", e);
+            }
+
+            mVolumeInfos[i].mCurrent = String.valueOf(current);
+            mVolumeInfos[i].mMax = String.valueOf(max);
+
+            mVolumeInfos[i].mLogicalMax = String.valueOf(mAudioManager.getStreamMaxVolume(stream));
+            mVolumeInfos[i].mLogicalCurrent = String.valueOf(mAudioManager.getStreamVolume(stream));
+
+            Log.d(TAG, stream + " max: " + mVolumeInfos[i].mMax + " current: "
+                    + mVolumeInfos[i].mCurrent);
+            i++;
+        }
+        mAdapter.refreshVolumes(mVolumeInfos);
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mCar != null) {
+            mCar.disconnect();
+        }
+        super.onDestroy();
+    }
+
+    private class VolumeController extends IVolumeController.Stub {
+
+        @Override
+        public void displaySafeVolumeWarning(int flags) throws RemoteException {}
+
+        @Override
+        public void volumeChanged(int streamType, int flags) throws RemoteException {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED, streamType));
+        }
+
+        @Override
+        public void masterMuteChanged(int flags) throws RemoteException {}
+
+        @Override
+        public void setLayoutDirection(int layoutDirection) throws RemoteException {
+        }
+
+        @Override
+        public void dismiss() throws RemoteException {
+        }
+    }
+}
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/carservice_test/src/com/android/car/test/CarHvacManagerTest.java b/tests/carservice_test/src/com/android/car/test/CarHvacManagerTest.java
index ef16acd..0b28e8b 100644
--- a/tests/carservice_test/src/com/android/car/test/CarHvacManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/test/CarHvacManagerTest.java
@@ -189,7 +189,9 @@
 
         @Override
         public synchronized VehiclePropValue onPropertyGet(VehiclePropValue value) {
-            return mMap.get(value.getProp());
+            VehiclePropValue currentValue = mMap.get(value.getProp());
+            // VNS will call getProperty method when subscribe is called, just return empty value.
+            return currentValue != null ? currentValue : value;
         }
 
         @Override
diff --git a/tests/carservice_test/src/com/android/car/test/CarRadioManagerTest.java b/tests/carservice_test/src/com/android/car/test/CarRadioManagerTest.java
index 36a8cfc..70c5474 100644
--- a/tests/carservice_test/src/com/android/car/test/CarRadioManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/test/CarRadioManagerTest.java
@@ -89,7 +89,11 @@
             value.getInt32ValuesList().toArray(valueList);
 
             // Get the actual preset.
-            if (valueList[0] < 1 || valueList[0] > NUM_PRESETS) return null;
+            if (valueList[0] < 1 || valueList[0] > NUM_PRESETS) {
+                // VNS will call getProperty method when subscribe is called, just return an empty
+                // value.
+                return value;
+            }
             CarRadioPreset preset = mRadioPresets.get(valueList[0]);
             VehiclePropValue v =
                 VehiclePropValueUtil.createIntVectorValue(
@@ -171,9 +175,9 @@
     }
 
     public void testSubscribe() throws Exception {
-        EventListener l = new EventListener();
+        EventListener listener = new EventListener();
         assertEquals("Lock should be freed by now.", 0, mAvailable.availablePermits());
-        mCarRadioManager.registerListener(l);
+        mCarRadioManager.registerListener(listener);
 
         // Wait for acquire to be available again, fail if timeout.
         boolean success = mAvailable.tryAcquire(5L, TimeUnit.SECONDS);
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);
+    }
+
+
+}
diff --git a/vehicle_network_service/VehicleNetworkService.cpp b/vehicle_network_service/VehicleNetworkService.cpp
index 4e0f77c..c13d3fc 100644
--- a/vehicle_network_service/VehicleNetworkService.cpp
+++ b/vehicle_network_service/VehicleNetworkService.cpp
@@ -151,6 +151,11 @@
     }
 }
 
+void VehicleHalMessageHandler::dump(String8& msg) {
+    msg.appendFormat("mFreeListIndex:%d, mLastDispatchTime:%" PRId64 "\n",
+                     mFreeListIndex, mLastDispatchTime);
+}
+
 // ----------------------------------------------------------------------------
 
 void MockDeathHandler::binderDied(const wp<IBinder>& who) {
@@ -218,6 +223,8 @@
         return NO_ERROR;
     }
     msg.append("MockingEnabled=%d\n", mMockingEnabled ? 1 : 0);
+    msg.appendFormat("*Handler, now in ms:%" PRId64 "\n", elapsedRealtime());
+    mHandler->dump(msg);
     msg.append("*Properties\n");
     for (auto& prop : mProperties->getList()) {
         VechilePropertyUtil::dumpProperty(msg, *prop);
@@ -231,8 +238,7 @@
     }
     msg.append("*Active clients*\n");
     for (size_t i = 0; i < mBinderToClientMap.size(); i++) {
-        msg.appendFormat("pid %d uid %d\n", mBinderToClientMap.valueAt(i)->getPid(),
-                mBinderToClientMap.valueAt(i)->getUid());
+        mBinderToClientMap.valueAt(i)->dump(msg);
     }
     msg.append("*Active clients per property*\n");
     for (size_t i = 0; i < mPropertyToClientsMap.size(); i++) {
@@ -249,15 +255,19 @@
         msg.appendFormat("prop 0x%x, sample rate %f Hz, zones 0x%x\n", mSubscriptionInfos.keyAt(i),
                 info.sampleRate, info.zones);
     }
-    msg.append("*Event counts per property*\n");
-    for (size_t i = 0; i < mEventsCount.size(); i++) {
-        msg.appendFormat("prop 0x%x: %d\n", mEventsCount.keyAt(i),
-                mEventsCount.valueAt(i));
+    msg.appendFormat("*Event info per property, now in ns:%" PRId64 " *\n", elapsedRealtimeNano());
+    for (size_t i = 0; i < mEventInfos.size(); i++) {
+        const EventInfo& info = mEventInfos.valueAt(i);
+        msg.appendFormat("prop 0x%x, event counts:%d, last timestamp: %" PRId64 "\n",
+                mEventInfos.keyAt(i), info.eventCount, info.lastTimestamp);
     }
+    msg.appendFormat(" Events dropped while in mocking:%d, last dropped time %" PRId64 "\n",
+                     mDroppedEventsWhileInMocking, mLastEventDropTimeWhileInMocking);
     msg.append("*Vehicle Network Service Permissions*\n");
     mVehiclePropertyAccessControl.dump(msg);
-
+    msg.append("*Vehicle HAL dump*\n");
     write(fd, msg.string(), msg.size());
+    mDevice->dump(mDevice, fd);
     return NO_ERROR;
 }
 
@@ -273,7 +283,9 @@
 
 VehicleNetworkService::VehicleNetworkService()
     : mModule(NULL),
-      mMockingEnabled(false) {
+      mMockingEnabled(false),
+      mDroppedEventsWhileInMocking(0),
+      mLastEventDropTimeWhileInMocking(0) {
     sInstance = this;
 
    // Load vehicle network services policy file
@@ -601,12 +613,14 @@
     bool shouldSubscribe = false;
     bool inMock = false;
     int32_t newZones = zones;
+    vehicle_prop_config_t const * config = NULL;
+    sp<HalClient> client;
     do {
         Mutex::Autolock autoLock(mLock);
         if (!isSubscribableLocked(prop)) {
             return BAD_VALUE;
         }
-        vehicle_prop_config_t const * config = findConfigLocked(prop);
+        config = findConfigLocked(prop);
         if (config->change_mode == VEHICLE_PROP_CHANGE_MODE_ON_CHANGE) {
             if (sampleRate != 0) {
                 ALOGW("Sample rate set to non-zeo for on change type. Ignore it");
@@ -635,7 +649,7 @@
         }
         sp<IBinder> ibinder = IInterface::asBinder(listener);
         LOG_VERBOSE("subscribe, binder 0x%x prop 0x%x", ibinder.get(), prop);
-        sp<HalClient> client = findOrCreateClientLocked(ibinder, listener);
+        client = findOrCreateClientLocked(ibinder, listener);
         if (client.get() == NULL) {
             ALOGE("subscribe, no memory, cannot create HalClient");
             return NO_MEMORY;
@@ -673,22 +687,73 @@
         }
     } while (false);
     if (shouldSubscribe) {
-        status_t r;
         if (inMock) {
-            r = mHalMock->onPropertySubscribe(prop, sampleRate, newZones);
+            status_t r = mHalMock->onPropertySubscribe(prop, sampleRate, newZones);
             if (r != NO_ERROR) {
                 ALOGW("subscribe 0x%x failed, mock returned %d", prop, r);
+                return r;
             }
         } else {
             LOG_VERBOSE("subscribe to HAL, prop 0x%x sample rate:%f zones:0x%x", prop, sampleRate,
                     newZones);
-            r = mDevice->subscribe(mDevice, prop, sampleRate, newZones);
+            status_t r = mDevice->subscribe(mDevice, prop, sampleRate, newZones);
             if (r != NO_ERROR) {
                 ALOGW("subscribe 0x%x failed, HAL returned %d", prop, r);
+                return r;
             }
         }
-        return r;
     }
+    if (config->change_mode == VEHICLE_PROP_CHANGE_MODE_ON_CHANGE) {
+        status_t r = notifyClientWithCurrentValue(inMock, config, zones);
+        if (r != NO_ERROR) {
+            return r;
+        }
+    }
+    return NO_ERROR;
+}
+
+status_t VehicleNetworkService::notifyClientWithCurrentValue(bool isMocking,
+        const vehicle_prop_config_t *config, int32_t zones) {
+    status_t r;
+    int32_t prop = config->prop;
+    int32_t valueType = config->value_type;
+    if (isZonedProperty(config)) {
+        int32_t requestedZones = (zones == 0) ? config->vehicle_zone_flags : zones;
+        for (int i = 0, zone = 1; i < 32; i++, zone <<= 1) {
+            if ((zone & requestedZones) == zone) {
+                    r = notifyClientWithCurrentValue(isMocking, prop, valueType, zone);
+                    if (r != NO_ERROR) {
+                        return r;
+                    }
+                }
+            }
+        } else {
+            r = notifyClientWithCurrentValue(isMocking, prop, valueType, 0 /*no zones*/);
+            if (r != NO_ERROR) {
+                return r;
+            }
+        }
+    return NO_ERROR;
+}
+
+status_t VehicleNetworkService::notifyClientWithCurrentValue(bool isMocking,
+    int32_t prop, int32_t valueType, int32_t zone) {
+    vehicle_prop_value_t value;
+    value.prop = prop;
+    value.value_type = valueType;
+    value.zone = zone;
+    status_t r = isMocking ? mHalMock->onPropertyGet(&value) : mDevice->get(mDevice, &value);
+    if (r != NO_ERROR) {
+        if (r == -EAGAIN) {
+            LOG_VERBOSE("value is not ready:0x%x, mock:%d", prop, isMocking);
+            return NO_ERROR;
+        } else {
+            ALOGW("failed to get current value prop:0x%x, mock:%d, error:%d", prop, isMocking, r);
+            return r;
+        }
+    }
+
+    onHalEvent(&value, false);
     return NO_ERROR;
 }
 
@@ -872,7 +937,7 @@
     // all subscriptions are invalid
     mPropertyToClientsMap.clear();
     mSubscriptionInfos.clear();
-    mEventsCount.clear();
+    mEventInfos.clear();
     List<sp<HalClient> > clientsToRemove;
     for (size_t i = 0; i < mBinderToClientMap.size(); i++) {
         sp<HalClient> client = mBinderToClientMap.valueAt(i);
@@ -961,16 +1026,19 @@
         if (!isInjection) {
             if (mMockingEnabled) {
                 // drop real HAL event if mocking is enabled
+                mDroppedEventsWhileInMocking++;
+                mLastEventDropTimeWhileInMocking = elapsedRealtimeNano();
                 return NO_ERROR;
             }
         }
-        ssize_t index = mEventsCount.indexOfKey(eventData->prop);
+        ssize_t index = mEventInfos.indexOfKey(eventData->prop);
         if (index < 0) {
-            mEventsCount.add(eventData->prop, 1);
+            EventInfo info(eventData->timestamp, 1);
+            mEventInfos.add(eventData->prop, info);
         } else {
-            int count = mEventsCount.valueAt(index);
-            count++;
-            mEventsCount.add(eventData->prop, count);
+            EventInfo& info = mEventInfos.editValueAt(index);
+            info.eventCount++;
+            info.lastTimestamp = eventData->timestamp;
         }
         handler = mHandler;
     } while (false);
@@ -1027,9 +1095,10 @@
     } while (false);
     EVENT_LOG("dispatchHalEvents num events %d, active clients:%d", events.size(),
             activeClients.size());
+    int64_t now = elapsedRealtimeNano();
     for (size_t i = 0; i < activeClients.size(); i++) {
         sp<HalClient> client = activeClients.editItemAt(i);
-        client->dispatchEvents();
+        client->dispatchEvents(now);
     }
     activeClients.clear();
 }
diff --git a/vehicle_network_service/VehicleNetworkService.h b/vehicle_network_service/VehicleNetworkService.h
index 2116f55..5798e7f 100644
--- a/vehicle_network_service/VehicleNetworkService.h
+++ b/vehicle_network_service/VehicleNetworkService.h
@@ -18,6 +18,7 @@
 #define CAR_VEHICLE_NETWORK_SERVICE_H_
 
 #include <stdint.h>
+#include <inttypes.h>
 #include <sys/types.h>
 
 #include <memory>
@@ -74,6 +75,7 @@
     void handleHalEvent(vehicle_prop_value_t *eventData);
     void handleHalError(VehicleHalError* error);
     void handleMockStart();
+    void dump(String8& msg);
 
 private:
     void handleMessage(const Message& message);
@@ -106,6 +108,21 @@
 };
 
 // ----------------------------------------------------------------------------
+class EventInfo {
+public:
+    int64_t lastTimestamp;
+    int eventCount;
+    EventInfo()
+        : lastTimestamp(0),
+          eventCount(0) {};
+    EventInfo(int64_t aLastTimestamp, int aEventCount)
+        : lastTimestamp(aLastTimestamp),
+          eventCount(aEventCount) {};
+    EventInfo(const EventInfo& info)
+        : lastTimestamp(info.lastTimestamp),
+          eventCount(info.eventCount) {};
+};
+// ----------------------------------------------------------------------------
 
 class HalClient : public virtual RefBase {
 public:
@@ -114,7 +131,10 @@
         mPid(pid),
         mUid(uid),
         mMonitoringHalRestart(false),
-        mMonitoringHalError(false) {
+        mMonitoringHalError(false),
+        mLastDispatchedEventCounts(0),
+        mTotalDispatchedEvents(0),
+        mLastDispatchTime(0) {
     }
 
     ~HalClient() {
@@ -204,11 +224,14 @@
     }
 
     // no lock here as this should be called only from single event looper thread
-    status_t dispatchEvents(){
+    status_t dispatchEvents(const int64_t& timestamp){
         ALOGV("dispatchEvents, num Events:%d", mEvents.size());
         sp<VehiclePropValueListHolder> events(new VehiclePropValueListHolder(&mEvents,
                 false /*deleteInDestructor */));
         ASSERT_OR_HANDLE_NO_MEMORY(events.get(), return NO_MEMORY);
+        mLastDispatchTime = timestamp;
+        mLastDispatchedEventCounts = mEvents.size();
+        mTotalDispatchedEvents += mLastDispatchedEventCounts;
         mListener->onEvents(events);
         mEvents.clear();
         return NO_ERROR;
@@ -222,6 +245,12 @@
         mListener->onHalRestart(inMocking);
     }
 
+    void dump(String8& msg) {
+        msg.appendFormat("pid:%d, uid:%d, mLastDispatchedEventCounts:%d, mTotalDispatchedEvents:%d"
+                ", mLastDispatchTime:%" PRId64 "\n",
+                mPid, mUid, mLastDispatchedEventCounts, mTotalDispatchedEvents, mLastDispatchTime);
+    }
+
 private:
     mutable Mutex mLock;
     const sp<IVehicleNetworkListener> mListener;
@@ -231,6 +260,9 @@
     List<vehicle_prop_value_t*> mEvents;
     bool mMonitoringHalRestart;
     bool mMonitoringHalError;
+    int mLastDispatchedEventCounts;
+    int mTotalDispatchedEvents;
+    int64_t mLastDispatchTime;
 };
 
 class HalClientSpVector : public SortedVector<sp<HalClient> >, public RefBase {
@@ -322,6 +354,7 @@
     bool isGettableLocked(int32_t property);
     bool isSettableLocked(int32_t property, int32_t valueType);
     bool isSubscribableLocked(int32_t property);
+    status_t getProperty(vehicle_prop_value_t *data, bool retry);
     static bool isZonedProperty(vehicle_prop_config_t const * config);
     sp<HalClient> findClientLocked(sp<IBinder>& ibinder);
     sp<HalClient> findOrCreateClientLocked(sp<IBinder>& ibinder,
@@ -331,6 +364,11 @@
     bool removePropertyFromClientLocked(sp<IBinder>& ibinder, sp<HalClient>& client,
             int32_t property);
     void handleHalRestartAndGetClientsToDispatchLocked(List<sp<HalClient> >& clientsToDispatch);
+    status_t notifyClientWithCurrentValue(bool isMocking, const vehicle_prop_config_t *config,
+                                          int32_t zones);
+    status_t notifyClientWithCurrentValue(bool isMocking, int32_t prop, int32_t valueType,
+                                          int32_t zone);
+
 
     static int eventCallback(const vehicle_prop_value_t *eventData);
     static int errorCallback(int32_t errorCode, int32_t property, int32_t operation);
@@ -350,9 +388,11 @@
     // client subscribing properties
     KeyedVector<int32_t, sp<HalClientSpVector> > mPropertyToClientsMap;
     KeyedVector<int32_t, SubscriptionInfo> mSubscriptionInfos;
-    KeyedVector<int32_t, int> mEventsCount;
+    KeyedVector<int32_t, EventInfo> mEventInfos;
     PropertyValueCache mCache;
     bool mMockingEnabled;
+    int mDroppedEventsWhileInMocking;
+    int64_t mLastEventDropTimeWhileInMocking;
     sp<IVehicleNetworkHalMock> mHalMock;
     sp<VehiclePropertiesHolder> mPropertiesForMocking;
     sp<MockDeathHandler> mHalMockDeathHandler;