Merge "Make drivingstate parcelable classes final."
diff --git a/car-lib/Android.bp b/car-lib/Android.bp
index 1d887fa..4e03d8c 100644
--- a/car-lib/Android.bp
+++ b/car-lib/Android.bp
@@ -109,17 +109,6 @@
     ],
 }
 
-genrule {
-    name: "android-car-last-released-test-api",
-    srcs: [
-        "api/test-released/*.txt",
-    ],
-    cmd: "cp -f $$(echo $(in) | tr \" \" \"\\n\" | sort -n | tail -1) $(genDir)/last-released-test-api.txt",
-    out: [
-        "last-released-test-api.txt",
-    ],
-}
-
 droidstubs {
     name: "android.car-stubs-docs",
     defaults: ["android.car-docs-default"],
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index 239ea00..19823a8 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -144,6 +144,7 @@
     field public static final int EV_CHARGE_PORT_OPEN = 287310602; // 0x1120030a
     field public static final int FOG_LIGHTS_STATE = 289410562; // 0x11400e02
     field public static final int FOG_LIGHTS_SWITCH = 289410578; // 0x11400e12
+    field public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364; // 0x11200604
     field public static final int FUEL_DOOR_OPEN = 287310600; // 0x11200308
     field public static final int FUEL_LEVEL = 291504903; // 0x11600307
     field public static final int FUEL_LEVEL_LOW = 287310853; // 0x11200405
@@ -394,28 +395,8 @@
 package android.car.media {
 
   public final class CarAudioManager {
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) public android.car.media.CarAudioPatchHandle createAudioPatch(String, int, int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) @NonNull public String[] getExternalSources();
-    method public int getGroupMaxVolume(int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMaxVolume(int, int);
-    method public int getGroupMinVolume(int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMinVolume(int, int);
-    method public int getGroupVolume(int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupVolume(int, int);
-    method @NonNull public int[] getUsagesForVolumeGroupId(int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) @NonNull public int[] getUsagesForVolumeGroupId(int, int);
-    method public int getVolumeGroupCount();
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupCount(int);
-    method public int getVolumeGroupIdForUsage(int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupIdForUsage(int, int);
     method public void registerCarVolumeCallback(@NonNull android.car.media.CarAudioManager.CarVolumeCallback);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) public void releaseAudioPatch(android.car.media.CarAudioPatchHandle);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setBalanceTowardRight(float);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setFadeTowardFront(float);
-    method public void setGroupVolume(int, int, int);
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setGroupVolume(int, int, int, int);
     method public void unregisterCarVolumeCallback(@NonNull android.car.media.CarAudioManager.CarVolumeCallback);
-    field public static final int PRIMARY_AUDIO_ZONE = 0; // 0x0
   }
 
   public abstract static class CarAudioManager.CarVolumeCallback {
@@ -424,13 +405,6 @@
     method public void onMasterMuteChanged(int, int);
   }
 
-  public final class CarAudioPatchHandle implements android.os.Parcelable {
-    ctor public CarAudioPatchHandle(android.media.AudioPatch);
-    method public int describeContents();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.car.media.CarAudioPatchHandle> CREATOR;
-  }
-
 }
 
 package android.car.navigation {
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index e468d3b..905e0f5 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -17,6 +17,7 @@
     field public static final String PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL = "android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
     field public static final String PERMISSION_CAR_POWER = "android.car.permission.CAR_POWER";
     field public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
+    field public static final String PERMISSION_CAR_PROJECTION_STATUS = "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
     field public static final String PERMISSION_CAR_TEST_SERVICE = "android.car.permission.CAR_TEST_SERVICE";
     field public static final String PERMISSION_CONTROL_APP_BLOCKING = "android.car.permission.CONTROL_APP_BLOCKING";
     field public static final String PERMISSION_CONTROL_CAR_CLIMATE = "android.car.permission.CONTROL_CAR_CLIMATE";
@@ -43,15 +44,19 @@
   }
 
   public final class CarProjectionManager {
-    method public void onCarDisconnected();
-    method public void registerProjectionListener(android.car.CarProjectionManager.CarProjectionListener, int);
-    method public void registerProjectionRunner(android.content.Intent);
-    method public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
-    method public boolean requestBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
-    method public void startProjectionAccessPoint(android.car.CarProjectionManager.ProjectionAccessPointCallback);
-    method public void stopProjectionAccessPoint();
-    method public void unregisterProjectionListener();
-    method public void unregisterProjectionRunner(android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public java.util.List<java.lang.Integer> getAvailableWifiChannels(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public android.os.Bundle getProjectionOptions();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionListener(@NonNull android.car.CarProjectionManager.CarProjectionListener, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionRunner(@NonNull android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void registerProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean requestBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int, @NonNull android.os.IBinder);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void startProjectionAccessPoint(@NonNull android.car.CarProjectionManager.ProjectionAccessPointCallback);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void stopProjectionAccessPoint();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionListener();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionRunner(@NonNull android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void unregisterProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void updateProjectionStatus(@NonNull android.car.projection.ProjectionStatus);
     field public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 2; // 0x2
     field public static final int PROJECTION_VOICE_SEARCH = 1; // 0x1
   }
@@ -71,6 +76,10 @@
     field public static final int ERROR_TETHERING_DISALLOWED = 4; // 0x4
   }
 
+  public static interface CarProjectionManager.ProjectionStatusListener {
+    method public void onProjectionStatusChanged(int, @Nullable String, @NonNull java.util.List<android.car.projection.ProjectionStatus>);
+  }
+
   public final class VehicleAreaDoor {
     field public static final int DOOR_HOOD = 268435456; // 0x10000000
     field public static final int DOOR_REAR = 536870912; // 0x20000000
@@ -169,6 +178,7 @@
 
   public abstract class InstrumentClusterRenderingService extends android.app.Service {
     ctor public InstrumentClusterRenderingService();
+    method @Nullable public android.graphics.Bitmap getBitmap(android.net.Uri);
     method @MainThread @Nullable public abstract android.car.cluster.renderer.NavigationRenderer getNavigationRenderer();
     method @CallSuper public android.os.IBinder onBind(android.content.Intent);
     method @MainThread public void onKeyEvent(@NonNull android.view.KeyEvent);
@@ -735,6 +745,40 @@
 
 }
 
+package android.car.media {
+
+  public final class CarAudioManager {
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) public android.car.media.CarAudioPatchHandle createAudioPatch(String, int, int);
+    method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) public String[] getExternalSources();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMaxVolume(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMaxVolume(int, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMinVolume(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupMinVolume(int, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupVolume(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getGroupVolume(int, int);
+    method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int[] getUsagesForVolumeGroupId(int);
+    method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int[] getUsagesForVolumeGroupId(int, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupCount();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupCount(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupIdForUsage(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public int getVolumeGroupIdForUsage(int, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) public void releaseAudioPatch(android.car.media.CarAudioPatchHandle);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setBalanceTowardRight(float);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setFadeTowardFront(float);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setGroupVolume(int, int, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) public void setGroupVolume(int, int, int, int);
+    field public static final int PRIMARY_AUDIO_ZONE = 0; // 0x0
+  }
+
+  public final class CarAudioPatchHandle implements android.os.Parcelable {
+    ctor public CarAudioPatchHandle(android.media.AudioPatch);
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.car.media.CarAudioPatchHandle> CREATOR;
+  }
+
+}
+
 package android.car.navigation {
 
   public class CarNavigationInstrumentCluster implements android.os.Parcelable {
@@ -756,6 +800,66 @@
 
 }
 
+package android.car.projection {
+
+  public class ProjectionOptions {
+    ctor public ProjectionOptions(android.os.Bundle);
+    method @Nullable public android.app.ActivityOptions getActivityOptions();
+    method @Nullable public android.content.ComponentName getConsentActivity();
+    method public int getUiMode();
+    method @NonNull public android.os.Bundle toBundle();
+    field public static final int UI_MODE_BLENDED = 1; // 0x1
+    field public static final int UI_MODE_FULL_SCREEN = 0; // 0x0
+  }
+
+  public final class ProjectionStatus implements android.os.Parcelable {
+    method @NonNull public static android.car.projection.ProjectionStatus.Builder builder(String, int);
+    method public int describeContents();
+    method @NonNull public java.util.List<android.car.projection.ProjectionStatus.MobileDevice> getConnectedMobileDevices();
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getPackageName();
+    method public int getState();
+    method public int getTransport();
+    method public boolean isActive();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus> CREATOR;
+    field public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3; // 0x3
+    field public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2; // 0x2
+    field public static final int PROJECTION_STATE_INACTIVE = 0; // 0x0
+    field public static final int PROJECTION_STATE_READY_TO_PROJECT = 1; // 0x1
+    field public static final int PROJECTION_TRANSPORT_NONE = 0; // 0x0
+    field public static final int PROJECTION_TRANSPORT_USB = 1; // 0x1
+    field public static final int PROJECTION_TRANSPORT_WIFI = 2; // 0x2
+  }
+
+  public static final class ProjectionStatus.Builder {
+    method @NonNull public android.car.projection.ProjectionStatus.Builder addMobileDevice(android.car.projection.ProjectionStatus.MobileDevice);
+    method public android.car.projection.ProjectionStatus build();
+    method @NonNull public android.car.projection.ProjectionStatus.Builder setExtras(android.os.Bundle);
+    method @NonNull public android.car.projection.ProjectionStatus.Builder setProjectionTransport(int);
+  }
+
+  public static final class ProjectionStatus.MobileDevice implements android.os.Parcelable {
+    method @NonNull public static android.car.projection.ProjectionStatus.MobileDevice.Builder builder(int, String);
+    method public int describeContents();
+    method @NonNull public java.util.List<java.lang.Integer> getAvailableTransports();
+    method @NonNull public android.os.Bundle getExtras();
+    method public int getId();
+    method @NonNull public String getName();
+    method public boolean isProjecting();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus.MobileDevice> CREATOR;
+  }
+
+  public static final class ProjectionStatus.MobileDevice.Builder {
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder addTransport(int);
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice build();
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setExtras(android.os.Bundle);
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setProjecting(boolean);
+  }
+
+}
+
 package android.car.storagemonitoring {
 
   public final class CarStorageMonitoringManager {
@@ -895,10 +999,10 @@
 package android.car.trust {
 
   public final class CarTrustAgentEnrollmentManager {
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void activateToken(long);
     method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void enrollmentHandshakeAccepted();
-    method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public java.util.List<java.lang.Integer> getEnrollmentHandlesForUser(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) @NonNull public java.util.List<java.lang.Long> getEnrollmentHandlesForUser(int);
     method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void initiateEnrollmentHandshake(android.bluetooth.BluetoothDevice);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public boolean isEscrowTokenActive(long, int);
     method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void revokeTrust(long);
     method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void setBleCallback(@Nullable android.car.trust.CarTrustAgentEnrollmentManager.CarTrustAgentBleCallback);
     method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void setEnrollmentCallback(@Nullable android.car.trust.CarTrustAgentEnrollmentManager.CarTrustAgentEnrollmentCallback);
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index ab6aa9f..0ab23b7 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -393,6 +393,14 @@
     public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
 
     /**
+     * Permission necessary to access projection status.
+     * @hide
+     */
+    @SystemApi
+    public static final String PERMISSION_CAR_PROJECTION_STATUS =
+            "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
+
+    /**
      * Permission necessary to mock vehicle hal for testing.
      * @hide
      * @deprecated mocking vehicle HAL in car service is no longer supported.
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 50606de..7706bf7 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -16,11 +16,18 @@
 
 package android.car;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
 import android.content.Intent;
 import android.net.wifi.WifiConfiguration;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -29,7 +36,14 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.util.Preconditions;
+
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * CarProjectionManager allows applications implementing projection to register/unregister itself
@@ -44,6 +58,9 @@
 public final class CarProjectionManager implements CarManagerBase {
     private static final String TAG = CarProjectionManager.class.getSimpleName();
 
+    private final Binder mToken = new Binder();
+    private final Object mLock = new Object();
+
     /**
      * Listener to get projected notifications.
      *
@@ -81,10 +98,30 @@
 
     private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy;
 
+    private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>();
+    private CarProjectionStatusListenerImpl mCarProjectionStatusListener;
+
     // Only one access point proxy object per process.
     private static final IBinder mAccessPointProxyToken = new Binder();
 
     /**
+     * Interface to receive for projection status updates.
+     */
+    public interface ProjectionStatusListener {
+        /**
+         * This method gets invoked if projection status has been changed.
+         *
+         * @param state - current projection state
+         * @param packageName - if projection is currently running either in the foreground or
+         *                      in the background this argument will contain its package name
+         * @param details - contains detailed information about all currently registered projection
+         *                  receivers.
+         */
+        void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName,
+                @NonNull List<ProjectionStatus> details);
+    }
+
+    /**
      * @hide
      */
     public CarProjectionManager(IBinder service, Handler handler) {
@@ -107,11 +144,11 @@
      * @param listener
      * @param voiceSearchFilter Flags of voice search requests to get notification.
      */
-    public void registerProjectionListener(CarProjectionListener listener, int voiceSearchFilter) {
-        if (listener == null) {
-            throw new IllegalArgumentException("null listener");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void registerProjectionListener(@NonNull CarProjectionListener listener,
+            int voiceSearchFilter) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
             if (mListener == null || mVoiceSearchFilter != voiceSearchFilter) {
                 try {
                     mService.registerProjectionListener(mBinderListener, voiceSearchFilter);
@@ -135,8 +172,9 @@
     /**
      * Unregister listener and stop listening projection events.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public void unregisterProjectionListener() {
-        synchronized (this) {
+        synchronized (mLock) {
             try {
                 mService.unregisterProjectionListener(mBinderListener);
             } catch (RemoteException e) {
@@ -152,11 +190,10 @@
      * to create reverse binding.
      * @param serviceIntent
      */
-    public void registerProjectionRunner(Intent serviceIntent) {
-        if (serviceIntent == null) {
-            throw new IllegalArgumentException("null serviceIntent");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void registerProjectionRunner(@NonNull Intent serviceIntent) {
+        Preconditions.checkNotNull("serviceIntent cannot be null");
+        synchronized (mLock) {
             try {
                 mService.registerProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
@@ -170,11 +207,10 @@
      * reverse binding.
      * @param serviceIntent
      */
-    public void unregisterProjectionRunner(Intent serviceIntent) {
-        if (serviceIntent == null) {
-            throw new IllegalArgumentException("null serviceIntent");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void unregisterProjectionRunner(@NonNull Intent serviceIntent) {
+        Preconditions.checkNotNull("serviceIntent cannot be null");
+        synchronized (mLock) {
             try {
                 mService.unregisterProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
@@ -183,6 +219,7 @@
         }
     }
 
+    /** @hide */
     @Override
     public void onCarDisconnected() {
         // nothing to do
@@ -194,9 +231,13 @@
      *
      * <p>A process can have only one request to start an access point, subsequent call of this
      * method will invalidate previous calls.
+     *
+     * @param callback to receive notifications when access point status changed for the request
      */
-    public void startProjectionAccessPoint(ProjectionAccessPointCallback callback) {
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
+        Preconditions.checkNotNull(callback, "callback cannot be null");
+        synchronized (mLock) {
             Looper looper = mHandler.getLooper();
             ProjectionAccessPointCallbackProxy proxy =
                     new ProjectionAccessPointCallbackProxy(this, looper, callback);
@@ -210,11 +251,32 @@
     }
 
     /**
+     * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz,
+     * e.g. channel 1 will be represented as 2412 in the list.
+     *
+     * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*}
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public @NonNull List<Integer> getAvailableWifiChannels(int band) {
+        try {
+            int[] channels = mService.getAvailableWifiChannels(band);
+            List<Integer> channelList = new ArrayList<>(channels.length);
+            for (int v : channels) {
+                channelList.add(v);
+            }
+            return channelList;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Stop Wi-Fi Access Point for wireless projection receiver app.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public void stopProjectionAccessPoint() {
         ProjectionAccessPointCallbackProxy proxy;
-        synchronized (this) {
+        synchronized (mLock) {
             proxy = mProjectionAccessPointCallbackProxy;
             mProjectionAccessPointCallbackProxy = null;
         }
@@ -239,8 +301,11 @@
      *                owning the token dies, the request will automatically be released.
      * @return True if the profile was successfully inhibited, false if an error occurred.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public boolean requestBluetoothProfileInhibit(
-            BluetoothDevice device, int profile, IBinder token) {
+            @NonNull BluetoothDevice device, int profile, @NonNull IBinder token) {
+        Preconditions.checkNotNull(device, "device cannot be null");
+        Preconditions.checkNotNull(token, "token cannot be null");
         try {
             return mService.requestBluetoothProfileInhibit(device, profile, token);
         } catch (RemoteException e) {
@@ -258,8 +323,11 @@
      *                {@link #requestBluetoothProfileInhibit}.
      * @return True if the request was released, false if an error occurred.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public boolean releaseBluetoothProfileInhibit(
             BluetoothDevice device, int profile, IBinder token) {
+        Preconditions.checkNotNull(device, "device cannot be null");
+        Preconditions.checkNotNull(token, "token cannot be null");
         try {
             return mService.releaseBluetoothProfileInhibit(device, profile, token);
         } catch (RemoteException e) {
@@ -268,6 +336,108 @@
     }
 
     /**
+     * Call this method to report projection status of your app. The aggregated status (from other
+     * projection apps if available) will be broadcasted to interested parties.
+     *
+     * @param status the reported status that will be distributed to the interested listeners
+     *
+     * @see #registerProjectionListener(CarProjectionListener, int)
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void updateProjectionStatus(@NonNull ProjectionStatus status) {
+        Preconditions.checkNotNull(status, "status cannot be null");
+        try {
+            mService.updateProjectionStatus(status, mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register projection status listener. See {@link ProjectionStatusListener} for details. It is
+     * allowed to register multiple listeners.
+     *
+     * <p>Note: provided listener will be called immediately with the most recent status.
+     *
+     * @param listener the listener to receive notification for any projection status changes
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+    public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
+            mProjectionStatusListeners.add(listener);
+
+            if (mCarProjectionStatusListener == null) {
+                mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this);
+                try {
+                    mService.registerProjectionStatusListener(mCarProjectionStatusListener);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            } else {
+                // Already subscribed to Car Service, immediately notify listener with the current
+                // projection status in the event handler thread.
+                mHandler.post(() ->
+                        listener.onProjectionStatusChanged(
+                                mCarProjectionStatusListener.mCurrentState,
+                                mCarProjectionStatusListener.mCurrentPackageName,
+                                mCarProjectionStatusListener.mDetails));
+            }
+        }
+    }
+
+    /**
+     * Unregister provided listener from projection status notifications
+     *
+     * @param listener the listener for projection status notifications that was previously
+     * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)}
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+    public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
+            if (!mProjectionStatusListeners.remove(listener)
+                    || !mProjectionStatusListeners.isEmpty()) {
+                return;
+            }
+            unregisterProjectionStatusListenerFromCarServiceLocked();
+        }
+    }
+
+    private void unregisterProjectionStatusListenerFromCarServiceLocked() {
+        try {
+            mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
+            mCarProjectionStatusListener = null;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void handleProjectionStatusChanged(@ProjectionState int state,
+            String packageName, List<ProjectionStatus> details) {
+        List<ProjectionStatusListener> listeners;
+        synchronized (mLock) {
+            listeners = new ArrayList<>(mProjectionStatusListeners);
+        }
+        for (ProjectionStatusListener listener : listeners) {
+            listener.onProjectionStatusChanged(state, packageName, details);
+        }
+    }
+
+    /**
+     * Returns {@link Bundle} object that contains customization for projection app. This bundle
+     * can be parsed using {@link ProjectionOptions}.
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public @NonNull Bundle getProjectionOptions() {
+        try {
+            return mService.getProjectionOptions();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Callback class for applications to receive updates about the LocalOnlyHotspot status.
      */
     public abstract static class ProjectionAccessPointCallback {
@@ -345,7 +515,7 @@
 
     private void handleVoiceAssistantRequest(boolean fromLongPress) {
         CarProjectionListener listener;
-        synchronized (this) {
+        synchronized (mLock) {
             if (mListener == null) {
                 return;
             }
@@ -371,4 +541,34 @@
             manager.mHandler.post(() -> manager.handleVoiceAssistantRequest(fromLongPress));
         }
     }
+
+    private static class CarProjectionStatusListenerImpl
+            extends ICarProjectionStatusListener.Stub {
+
+        private @ProjectionState int mCurrentState;
+        private @Nullable String mCurrentPackageName;
+        private List<ProjectionStatus> mDetails = new ArrayList<>(0);
+
+        private final WeakReference<CarProjectionManager> mManagerRef;
+
+        private CarProjectionStatusListenerImpl(CarProjectionManager mgr) {
+            mManagerRef = new WeakReference<>(mgr);
+        }
+
+        @Override
+        public void onProjectionStatusChanged(int projectionState,
+                String packageName,
+                List<ProjectionStatus> details) {
+            CarProjectionManager mgr = mManagerRef.get();
+            if (mgr != null) {
+                mgr.mHandler.post(() -> {
+                    mCurrentState = projectionState;
+                    mCurrentPackageName = packageName;
+                    mDetails = Collections.unmodifiableList(details);
+
+                    mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails);
+                });
+            }
+        }
+    }
 }
diff --git a/car-lib/src/android/car/ICarProjection.aidl b/car-lib/src/android/car/ICarProjection.aidl
index efaac0d..6e85c45 100644
--- a/car-lib/src/android/car/ICarProjection.aidl
+++ b/car-lib/src/android/car/ICarProjection.aidl
@@ -17,8 +17,11 @@
 package android.car;
 
 import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionStatus;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.Messenger;
 
 /**
@@ -69,4 +72,22 @@
     /** Undo the effects of requestBluetoothProfileInhibit. */
     boolean releaseBluetoothProfileInhibit(
             in BluetoothDevice device, in int profile, in IBinder token) = 7;
+
+    /** Reports projection status for a given projection receiver app. */
+    void updateProjectionStatus(in ProjectionStatus status, in IBinder token) = 8;
+
+    /** Registers projection status listener */
+    void registerProjectionStatusListener(in ICarProjectionStatusListener listener) = 9;
+
+    /** Unregister projection status listener */
+    void unregisterProjectionStatusListener(in ICarProjectionStatusListener listener) = 10;
+
+    /**
+     * Returns options for projection receiver app that can be un-packed using
+     * {@link android.car.projection.ProjectionOptions} class.
+     */
+    Bundle getProjectionOptions() = 11;
+
+    /** Returns a list of available Wi-Fi channels */
+    int[] getAvailableWifiChannels(int band) = 12;
 }
diff --git a/car-lib/src/android/car/ICarProjectionStatusListener.aidl b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
new file mode 100644
index 0000000..cac48a0
--- /dev/null
+++ b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.car.projection.ProjectionStatus;
+
+/**
+ * Listener interface to notify interested parties of projection status change.
+ *
+ * @hide
+ */
+oneway interface ICarProjectionStatusListener {
+    void onProjectionStatusChanged(int projectionState,
+             in String activeProjectionPackageName,
+             in List<ProjectionStatus> details) = 0;
+}
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index ce10d2a..cfc6e34 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -295,6 +295,10 @@
      */
     public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
     /**
+     * Fuel consumption units for display
+     */
+    public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
+    /**
      * Outside temperature
      */
     public static final int ENV_OUTSIDE_TEMPERATURE = 291505923;
@@ -776,6 +780,9 @@
         if (o == EV_BATTERY_DISPLAY_UNITS) {
             return "EV_BATTERY_DISPLAY_UNITS";
         }
+        if (o == FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME) {
+            return "FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME";
+        }
         if (o == ENV_OUTSIDE_TEMPERATURE) {
             return "ENV_OUTSIDE_TEMPERATURE";
         }
diff --git a/car-lib/src/android/car/VehicleUnit.java b/car-lib/src/android/car/VehicleUnit.java
index c9859dc..463d53b 100644
--- a/car-lib/src/android/car/VehicleUnit.java
+++ b/car-lib/src/android/car/VehicleUnit.java
@@ -43,7 +43,8 @@
     public static final int KELVIN = 0x32;
     public static final int MILLILITER = 0x40;
     public static final int LITER = 0x41;
-    public static final int GALLON  = 0x42;
+    public static final int US_GALLON  = 0x42;
+    public static final int IMPERIAL_GALLON = 0x43;
     public static final int NANO_SECS = 0x50;
     public static final int SECS = 0x53;
     public static final int YEAR = 0x59;
@@ -74,7 +75,8 @@
             KELVIN,
             MILLILITER,
             LITER,
-            GALLON,
+            US_GALLON,
+            IMPERIAL_GALLON,
             NANO_SECS,
             SECS,
             YEAR,
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 4a9e5f5..f00d6ff 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -32,11 +32,16 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
@@ -47,14 +52,19 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
- * A service that used for interaction between Car Service and Instrument Cluster. Car Service may
+ * A service used for interaction between Car Service and Instrument Cluster. Car Service may
  * provide internal navigation binder interface to Navigation App and all notifications will be
  * eventually land in the {@link NavigationRenderer} returned by {@link #getNavigationRenderer()}.
  *
@@ -85,15 +95,45 @@
     private static class ContextOwner {
         final int mUid;
         final int mPid;
+        final Set<String> mPackageNames;
+        final Set<String> mAuthorities;
 
-        ContextOwner(int uid, int pid) {
+        ContextOwner(int uid, int pid, PackageManager packageManager) {
             mUid = uid;
             mPid = pid;
+            String[] packageNames = uid != 0 ? packageManager.getPackagesForUid(uid)
+                    : null;
+            mPackageNames = packageNames != null
+                    ? Collections.unmodifiableSet(new HashSet<>(Arrays.asList(packageNames)))
+                    : Collections.emptySet();
+            mAuthorities = Collections.unmodifiableSet(mPackageNames.stream()
+                    .map(packageName -> getAuthoritiesForPackage(packageManager, packageName))
+                    .flatMap(Collection::stream)
+                    .collect(Collectors.toSet()));
         }
 
         @Override
         public String toString() {
-            return "{uid: " + mUid + ", pid: " + mPid + "}";
+            return "{uid: " + mUid + ", pid: " + mPid + ", packagenames: " + mPackageNames
+                    + ", authorities: " + mAuthorities + "}";
+        }
+
+        private List<String> getAuthoritiesForPackage(PackageManager packageManager,
+                String packageName) {
+            try {
+                ProviderInfo[] providers = packageManager.getPackageInfo(packageName,
+                        PackageManager.GET_PROVIDERS).providers;
+                if (providers == null) {
+                    return Collections.emptyList();
+                }
+                return Arrays.stream(providers)
+                        .map(provider -> provider.authority)
+                        .collect(Collectors.toList());
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(TAG, "Package name not found while retrieving content provider authorities: "
+                        + packageName);
+                return Collections.emptyList();
+            }
         }
     }
 
@@ -199,7 +239,7 @@
      */
     @Nullable
     private ComponentName getNavigationComponentByOwner(ContextOwner contextOwner) {
-        for (String packageName : getPackageNamesForUid(contextOwner)) {
+        for (String packageName : contextOwner.mPackageNames) {
             ComponentName component = getComponentFromPackage(packageName);
             if (component != null) {
                 if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -211,14 +251,6 @@
         return null;
     }
 
-    private String[] getPackageNamesForUid(ContextOwner contextOwner) {
-        if (contextOwner == null || contextOwner.mUid == 0 || contextOwner.mPid == 0) {
-            return new String[0];
-        }
-        String[] packageNames  = getPackageManager().getPackagesForUid(contextOwner.mUid);
-        return packageNames != null ? packageNames : new String[0];
-    }
-
     private ContextOwner getNavigationContextOwner() {
         synchronized (mLock) {
             return mNavContextOwner;
@@ -338,8 +370,7 @@
         writer.println("activity options: " + mActivityOptions);
         writer.println("activity state: " + mActivityState);
         writer.println("current nav component: " + mNavigationComponent);
-        writer.println("current nav packages: " + Arrays.toString(getPackageNamesForUid(
-                getNavigationContextOwner())));
+        writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
     }
 
     private class RendererBinder extends IInstrumentCluster.Stub {
@@ -356,8 +387,11 @@
 
         @Override
         public void setNavigationContextOwner(int uid, int pid) throws RemoteException {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Updating navigation ownership to uid: " + uid + ", pid: " + pid);
+            }
             synchronized (mLock) {
-                mNavContextOwner = new ContextOwner(uid, pid);
+                mNavContextOwner = new ContextOwner(uid, pid, getPackageManager());
             }
             mUiHandler.post(InstrumentClusterRenderingService.this::updateNavigationActivity);
         }
@@ -419,4 +453,57 @@
         }
         return result.get();
     }
+
+    /**
+     * Fetches a bitmap from the navigation context owner (application holding navigation focus).
+     * It returns null if:
+     * <ul>
+     * <li>there is no navigation context owner
+     * <li>or if the {@link Uri} is invalid
+     * <li>or if it references a process other than the current navigation context owner
+     * </ul>
+     * This is a costly operation. Returned bitmaps should be cached and fetching should be done on
+     * a secondary thread.
+     */
+    @Nullable
+    public Bitmap getBitmap(Uri uri) {
+        try {
+            ContextOwner contextOwner = getNavigationContextOwner();
+            if (contextOwner == null) {
+                Log.e(TAG, "No context owner available while fetching: " + uri);
+                return null;
+            }
+
+            String host = uri.getHost();
+
+            if (!contextOwner.mAuthorities.contains(host)) {
+                Log.e(TAG, "Uri points to an authority not handled by the current context owner: "
+                        + uri + " (valid authorities: " + contextOwner.mAuthorities + ")");
+                return null;
+            }
+
+            // Add user to URI to make the request to the right instance of content provider
+            // (see ContentProvider#getUserIdFromAuthority()).
+            int userId = UserHandle.getUserId(contextOwner.mUid);
+            Uri filteredUid = uri.buildUpon().encodedAuthority(userId + "@" + host).build();
+
+            // Fetch the bitmap
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Requesting bitmap: " + uri);
+            }
+            ParcelFileDescriptor fileDesc = getContentResolver()
+                    .openFileDescriptor(filteredUid, "r");
+            if (fileDesc != null) {
+                Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
+                fileDesc.close();
+                return bitmap;
+            } else {
+                Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+            }
+        } catch (Throwable e) {
+            Log.e(TAG, "Unable to fetch uri: " + uri, e);
+        }
+
+        return null;
+    }
 }
diff --git a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
index 4681a8b..644e17e 100644
--- a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
+++ b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
@@ -18,7 +18,6 @@
 import android.annotation.SystemApi;
 import android.annotation.UiThread;
 import android.car.navigation.CarNavigationInstrumentCluster;
-import android.graphics.Bitmap;
 import android.os.Bundle;
 
 /**
@@ -32,10 +31,10 @@
     /**
      * Returns properties of instrument cluster for navigation.
      */
-    abstract public CarNavigationInstrumentCluster getNavigationProperties();
+    public abstract CarNavigationInstrumentCluster getNavigationProperties();
 
     /**
      * Called when an event is fired to change the navigation state.
      */
-    abstract public void onEvent(int eventType, Bundle bundle);
+    public abstract void onEvent(int eventType, Bundle bundle);
 }
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index d3fb4bd..9fb1fa5 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.car.Car;
 import android.car.CarLibLog;
@@ -52,7 +53,9 @@
 
     /**
      * Zone id of the primary audio zone.
+     * @hide
      */
+    @SystemApi
     public static final int PRIMARY_AUDIO_ZONE = 0x0;
 
     private final ICarAudio mService;
@@ -91,7 +94,10 @@
      * Sets the volume index for a volume group in primary zone.
      *
      * @see {@link #setGroupVolume(int, int, int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public void setGroupVolume(int groupId, int index, int flags) {
         setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, index, flags);
     }
@@ -105,7 +111,9 @@
      *            {@link #getGroupMaxVolume(int, 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})
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
         try {
@@ -119,7 +127,10 @@
      * Returns the maximum volume index for a volume group in primary zone.
      *
      * @see {@link #getGroupMaxVolume(int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupMaxVolume(int groupId) {
         return getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId);
     }
@@ -130,7 +141,9 @@
      * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose maximum volume index is returned.
      * @return The maximum valid volume index for the given group.
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupMaxVolume(int zoneId, int groupId) {
         try {
@@ -144,7 +157,10 @@
      * Returns the minimum volume index for a volume group in primary zone.
      *
      * @see {@link #getGroupMinVolume(int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupMinVolume(int groupId) {
         return getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
     }
@@ -155,7 +171,9 @@
      * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose minimum volume index is returned.
      * @return The minimum valid volume index for the given group, non-negative
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupMinVolume(int zoneId, int groupId) {
         try {
@@ -169,7 +187,10 @@
      * Returns the current volume index for a volume group in primary zone.
      *
      * @see {@link #getGroupVolume(int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupVolume(int groupId) {
         return getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
     }
@@ -183,7 +204,9 @@
      *
      * @see #getGroupMaxVolume(int, int)
      * @see #setGroupVolume(int, int, int, int)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getGroupVolume(int zoneId, int groupId) {
         try {
@@ -200,7 +223,9 @@
      *              fully toward the front.  0.0 means evenly balanced.
      *
      * @see #setBalanceTowardRight(float)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public void setFadeTowardFront(float value) {
         try {
@@ -217,7 +242,9 @@
      *              fully toward the right.  0.0 means evenly balanced.
      *
      * @see #setFadeTowardFront(float)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public void setBalanceTowardRight(float value) {
         try {
@@ -237,7 +264,9 @@
      *
      * @see #createAudioPatch(String, int, int)
      * @see #releaseAudioPatch(CarAudioPatchHandle)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
     public @NonNull String[] getExternalSources() {
         try {
@@ -264,7 +293,9 @@
      *
      * @see #getExternalSources()
      * @see #releaseAudioPatch(CarAudioPatchHandle)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
     public CarAudioPatchHandle createAudioPatch(String sourceAddress,
             @AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
@@ -283,7 +314,9 @@
      *
      * @see #getExternalSources()
      * @see #createAudioPatch(String, int, int)
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
     public void releaseAudioPatch(CarAudioPatchHandle patch) {
         try {
@@ -297,7 +330,10 @@
      * Gets the count of available volume groups in primary zone.
      *
      * @see {@link #getVolumeGroupCount(int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getVolumeGroupCount() {
         return getVolumeGroupCount(PRIMARY_AUDIO_ZONE);
     }
@@ -307,7 +343,9 @@
      *
      * @param zoneId The zone id whois count of volume groups is queried.
      * @return Count of volume groups
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getVolumeGroupCount(int zoneId) {
         try {
@@ -321,7 +359,10 @@
      * Gets the volume group id for a given {@link AudioAttributes} usage in primary zone.
      *
      * @see {@link #getVolumeGroupIdForUsage(int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) {
         return getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, usage);
     }
@@ -332,7 +373,9 @@
      * @param zoneId The zone id whose volume group is queried.
      * @param usage The {@link AudioAttributes} usage to get a volume group from.
      * @return The volume group id where the usage belongs to
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage) {
         try {
@@ -346,7 +389,10 @@
      * Gets array of {@link AudioAttributes} usages for a volume group in primary zone.
      *
      * @see {@link #getUsagesForVolumeGroupId(int, int)}
+     * @hide
      */
+    @SystemApi
+    @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public @NonNull int[] getUsagesForVolumeGroupId(int groupId) {
         return getUsagesForVolumeGroupId(PRIMARY_AUDIO_ZONE, groupId);
     }
@@ -357,7 +403,9 @@
      * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose associated audio usages is returned.
      * @return Array of {@link AudioAttributes} usages for a given volume group id
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
     public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
         try {
diff --git a/car-lib/src/android/car/media/CarAudioPatchHandle.java b/car-lib/src/android/car/media/CarAudioPatchHandle.java
index bbcc791..77dfc23 100644
--- a/car-lib/src/android/car/media/CarAudioPatchHandle.java
+++ b/car-lib/src/android/car/media/CarAudioPatchHandle.java
@@ -16,6 +16,7 @@
 
 package android.car.media;
 
+import android.annotation.SystemApi;
 import android.media.AudioDevicePort;
 import android.media.AudioPatch;
 import android.os.Parcel;
@@ -27,7 +28,9 @@
  * A class to encapsulate the handle for a system level audio patch. This is used
  * to provide a "safe" way for permitted applications to route automotive audio sources
  * outside of android.
+ * @hide
  */
+@SystemApi
 public final class CarAudioPatchHandle implements Parcelable {
 
     // This is enough information to uniquely identify a patch to the system
diff --git a/car-lib/src/android/car/projection/ProjectionOptions.java b/car-lib/src/android/car/projection/ProjectionOptions.java
new file mode 100644
index 0000000..26b8140
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionOptions.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class holds OEM customization for projection receiver app.  It is created by Car Service.
+ *
+ * @hide
+ */
+@SystemApi
+public class ProjectionOptions {
+    private static final String KEY_PREFIX = "android.car.projection.";
+
+    /** Immersive full screen mode (all system bars are hidden) */
+    public static final int UI_MODE_FULL_SCREEN = 0;
+
+    /** Show status and navigation bars. */
+    public static final int UI_MODE_BLENDED = 1;
+
+    private static final int UI_MODE_DEFAULT = UI_MODE_FULL_SCREEN;
+
+    /** @hide */
+    @IntDef({UI_MODE_FULL_SCREEN, UI_MODE_BLENDED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionUiMode {}
+
+    private static final String KEY_ACTIVITY_OPTIONS = KEY_PREFIX + "activityOptions";
+    private static final String KEY_UI_MODE = KEY_PREFIX + "systemUiFlags";
+    private static final String KEY_CONSENT_ACTIVITY = KEY_PREFIX + "consentActivity";
+
+    private final ActivityOptions mActivityOptions;
+    private final int mUiMode;
+    private final ComponentName mConsentActivity;
+
+    /**
+     * Creates new instance for given {@code Bundle}
+     *
+     * @param bundle contains OEM specific information
+     */
+    public ProjectionOptions(Bundle bundle) {
+        Bundle activityOptionsBundle = bundle.getBundle(KEY_ACTIVITY_OPTIONS);
+        mActivityOptions = activityOptionsBundle != null
+                ? new ActivityOptions(activityOptionsBundle) : null;
+        mUiMode = bundle.getInt(KEY_UI_MODE, UI_MODE_DEFAULT);
+        mConsentActivity = bundle.getParcelable(KEY_CONSENT_ACTIVITY);
+    }
+
+    private ProjectionOptions(Builder builder) {
+        mActivityOptions = builder.mActivityOptions;
+        mUiMode = builder.mUiMode;
+        mConsentActivity = builder.mConsentActivity;
+    }
+
+    /**
+     * Returns combination of flags from View.SYSTEM_UI_FLAG_* which will be used by projection
+     * receiver app during rendering.
+     */
+    public @ProjectionUiMode int getUiMode() {
+        return mUiMode;
+    }
+
+    /**
+     * Returns {@link ActivityOptions} that needs to be applied when launching projection activity
+     */
+    public @Nullable ActivityOptions getActivityOptions() {
+        return mActivityOptions;
+    }
+
+    /**
+     * Returns package/activity name of the consent activity provided by OEM which needs to be shown
+     * for all mobile devices unless user accepted the consent.
+     *
+     * <p>If the method returns null then consent dialog should not be shown.
+     */
+    public @Nullable ComponentName getConsentActivity() {
+        return mConsentActivity;
+    }
+
+    /** Converts current object to {@link Bundle} */
+    public @NonNull Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        if (mActivityOptions != null) {
+            bundle.putBundle(KEY_ACTIVITY_OPTIONS, mActivityOptions.toBundle());
+        }
+        bundle.putParcelable(KEY_CONSENT_ACTIVITY, mConsentActivity);
+        if (mUiMode != UI_MODE_DEFAULT) {
+            bundle.putInt(KEY_UI_MODE, mUiMode);
+        }
+        return bundle;
+    }
+
+    /** @hide */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** @hide */
+    public static class Builder {
+        private ActivityOptions mActivityOptions;
+        private int mUiMode = UI_MODE_DEFAULT;
+        private ComponentName mConsentActivity;
+
+        /** Sets {@link ActivityOptions} to launch projection activity. */
+        public Builder setProjectionActivityOptions(ActivityOptions activityOptions) {
+            mActivityOptions = activityOptions;
+            return this;
+        }
+
+        /** Set UI for projection activity. It can be one of {@code UI_MODE_*} constants. */
+        public Builder setUiMode(@ProjectionUiMode int uiMode) {
+            mUiMode = uiMode;
+            return this;
+        }
+
+        /** Sets consent activity which will be shown before starting projection. */
+        public Builder setConsentActivity(ComponentName consentActivity) {
+            mConsentActivity = consentActivity;
+            return this;
+        }
+
+        /** Creates an instance of {@link android.car.projection.ProjectionOptions} */
+        public ProjectionOptions build() {
+            return new ProjectionOptions(this);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        return toBundle().toString();
+    }
+}
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.aidl b/car-lib/src/android/car/projection/ProjectionStatus.aidl
new file mode 100644
index 0000000..f2ad77a
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.projection;
+
+parcelable ProjectionStatus;
\ No newline at end of file
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.java b/car-lib/src/android/car/projection/ProjectionStatus.java
new file mode 100644
index 0000000..37d0aee
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2019 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.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.IntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class encapsulates information about projection status and connected mobile devices.
+ *
+ * <p>Since the number of connected devices expected to be small we include information about
+ * connected devices in every status update.
+ *
+ * @hide
+ */
+@SystemApi
+public final class ProjectionStatus implements Parcelable {
+    /** This state indicates that projection is not actively running and no compatible mobile
+     * devices available. */
+    public static final int PROJECTION_STATE_INACTIVE = 0;
+
+    /** At least one phone connected and ready to project. */
+    public static final int PROJECTION_STATE_READY_TO_PROJECT = 1;
+
+    /** Projecting in the foreground */
+    public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2;
+
+    /** Projection is running in the background */
+    public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3;
+
+    private static final int PROJECTION_STATE_MAX = PROJECTION_STATE_ACTIVE_BACKGROUND;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_NONE = 0;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_USB = 1;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_WIFI = 2;
+
+    private static final int PROJECTION_TRANSPORT_MAX = PROJECTION_TRANSPORT_WIFI;
+
+    /** @hide */
+    @IntDef(value = {
+            PROJECTION_TRANSPORT_NONE,
+            PROJECTION_TRANSPORT_USB,
+            PROJECTION_TRANSPORT_WIFI,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionTransport {}
+
+    /** @hide */
+    @IntDef(value = {
+            PROJECTION_STATE_INACTIVE,
+            PROJECTION_STATE_READY_TO_PROJECT,
+            PROJECTION_STATE_ACTIVE_FOREGROUND,
+            PROJECTION_STATE_ACTIVE_BACKGROUND,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionState {}
+
+    private final String mPackageName;
+    private final int mState;
+    private final int mTransport;
+    private final List<MobileDevice> mConnectedMobileDevices;
+    private final Bundle mExtras;
+
+    /** Creator for this class. Required to have in parcelable implementations. */
+    public static final Creator<ProjectionStatus> CREATOR = new Creator<ProjectionStatus>() {
+        @Override
+        public ProjectionStatus createFromParcel(Parcel source) {
+            return new ProjectionStatus(source);
+        }
+
+        @Override
+        public ProjectionStatus[] newArray(int size) {
+            return new ProjectionStatus[size];
+        }
+    };
+
+    private ProjectionStatus(Builder builder) {
+        mPackageName = builder.mPackageName;
+        mState = builder.mState;
+        mTransport = builder.mTransport;
+        mConnectedMobileDevices = new ArrayList<>(builder.mMobileDevices);
+        mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+    }
+
+    private ProjectionStatus(Parcel source) {
+        mPackageName = source.readString();
+        mState = source.readInt();
+        mTransport = source.readInt();
+        mExtras = source.readBundle(getClass().getClassLoader());
+        mConnectedMobileDevices = source.createTypedArrayList(MobileDevice.CREATOR);
+    }
+
+    /** Parcelable implementation */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mPackageName);
+        dest.writeInt(mState);
+        dest.writeInt(mTransport);
+        dest.writeBundle(mExtras);
+        dest.writeTypedList(mConnectedMobileDevices);
+    }
+
+    /** Returns projection state which could be one of the constants starting with
+     * {@code #PROJECTION_STATE_}.
+     */
+    public @ProjectionState int getState() {
+        return mState;
+    }
+
+    /** Returns package name of the projection receiver app. */
+    public @NonNull String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns extra information provided by projection receiver app */
+    public @NonNull Bundle getExtras() {
+        return mExtras == null ? new Bundle() : new Bundle(mExtras);
+    }
+
+    /** Returns true if currently projecting either in the foreground or in the background. */
+    public boolean isActive() {
+        return mState == PROJECTION_STATE_ACTIVE_BACKGROUND
+                || mState == PROJECTION_STATE_ACTIVE_FOREGROUND;
+    }
+
+    /** Returns transport which is used for active projection or
+     * {@link #PROJECTION_TRANSPORT_NONE} if projection is not running.
+     */
+    public @ProjectionTransport int getTransport() {
+        return mTransport;
+    }
+
+    /** Returns a list of currently connected mobile devices. */
+    public @NonNull List<MobileDevice> getConnectedMobileDevices() {
+        return new ArrayList<>(mConnectedMobileDevices);
+    }
+
+    /**
+     * Returns new {@link Builder} instance.
+     *
+     * @param packageName package name that will be associated with this status
+     * @param state current projection state, must be one of the {@code PROJECTION_STATE_*}
+     */
+    @NonNull
+    public static Builder builder(String packageName, @ProjectionState int state) {
+        return new Builder(packageName, state);
+    }
+
+    /** Builder class for {@link ProjectionStatus} */
+    public static final class Builder {
+        private final int mState;
+        private final String mPackageName;
+        private int mTransport = PROJECTION_TRANSPORT_NONE;
+        private List<MobileDevice> mMobileDevices = new ArrayList<>();
+        private Bundle mExtras;
+
+        private Builder(String packageName, @ProjectionState int state) {
+            if (packageName == null) {
+                throw new IllegalArgumentException("Package name can't be null");
+            }
+            if (state < 0 || state > PROJECTION_STATE_MAX) {
+                throw new IllegalArgumentException("Invalid projection state: " + state);
+            }
+            mPackageName = packageName;
+            mState = state;
+        }
+
+        /**
+         * Sets the transport which is used for currently projecting phone if any.
+         *
+         * @param transport transport of current projection, must be one of the
+         * {@code PROJECTION_TRANSPORT_*}
+         */
+        public @NonNull Builder setProjectionTransport(@ProjectionTransport int transport) {
+            checkProjectionTransport(transport);
+            mTransport = transport;
+            return this;
+        }
+
+        /**
+         * Add connected mobile device
+         *
+         * @param mobileDevice connected mobile device
+         * @return this builder
+         */
+        public @NonNull Builder addMobileDevice(MobileDevice mobileDevice) {
+            mMobileDevices.add(mobileDevice);
+            return this;
+        }
+
+        /**
+         * Add extra information.
+         *
+         * @param extras may contain an extra information that can be passed from the projection
+         * app to the projection status listeners
+         * @return this builder
+         */
+        public @NonNull Builder setExtras(Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Creates {@link ProjectionStatus} object. */
+        public ProjectionStatus build() {
+            return new ProjectionStatus(this);
+        }
+    }
+
+    private static void checkProjectionTransport(@ProjectionTransport int transport) {
+        if (transport < 0 || transport > PROJECTION_TRANSPORT_MAX) {
+            throw new IllegalArgumentException("Invalid projection transport: " + transport);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ProjectionStatus{"
+                + "mPackageName='" + mPackageName + '\''
+                + ", mState=" + mState
+                + ", mTransport=" + mTransport
+                + ", mConnectedMobileDevices=" + mConnectedMobileDevices
+                + (mExtras != null ? " (has extras)" : "")
+                + '}';
+    }
+
+    /** Class that represents information about connected mobile device. */
+    public static final class MobileDevice implements Parcelable {
+        private final int mId;
+        private final String mName;
+        private final int[] mAvailableTransports;
+        private final boolean mProjecting;
+        private final Bundle mExtras;
+
+        /** Creator for this class. Required to have in parcelable implementations. */
+        public static final Creator<MobileDevice> CREATOR = new Creator<MobileDevice>() {
+            @Override
+            public MobileDevice createFromParcel(Parcel source) {
+                return new MobileDevice(source);
+            }
+
+            @Override
+            public MobileDevice[] newArray(int size) {
+                return new MobileDevice[size];
+            }
+        };
+
+        private MobileDevice(Builder builder) {
+            mId = builder.mId;
+            mName = builder.mName;
+            mAvailableTransports = builder.mAvailableTransports.toArray();
+            mProjecting = builder.mProjecting;
+            mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+        }
+
+        private MobileDevice(Parcel source) {
+            mId = source.readInt();
+            mName = source.readString();
+            mAvailableTransports = source.createIntArray();
+            mProjecting = source.readBoolean();
+            mExtras = source.readBundle(getClass().getClassLoader());
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mId);
+            dest.writeString(mName);
+            dest.writeIntArray(mAvailableTransports);
+            dest.writeBoolean(mProjecting);
+            dest.writeBundle(mExtras);
+        }
+
+        /** Returns the device id which uniquely identifies the mobile device within projection  */
+        public int getId() {
+            return mId;
+        }
+
+        /** Returns the name of the device */
+        public @NonNull String getName() {
+            return mName;
+        }
+
+        /** Returns a list of available projection transports. See {@code PROJECTION_TRANSPORT_*}
+         * for possible values. */
+        public @NonNull List<Integer> getAvailableTransports() {
+            List<Integer> transports = new ArrayList<>(mAvailableTransports.length);
+            for (int transport : mAvailableTransports) {
+                transports.add(transport);
+            }
+            return transports;
+        }
+
+        /** Indicates whether this mobile device is currently projecting */
+        public boolean isProjecting() {
+            return mProjecting;
+        }
+
+        /** Returns extra information for mobile device */
+        public @NonNull Bundle getExtras() {
+            return mExtras == null ? new Bundle() : new Bundle(mExtras);
+        }
+
+        /** Parcelable implementation */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /**
+         * Creates new instance of {@link Builder}
+         *
+         * @param id uniquely identifies the device
+         * @param name name of the connected device
+         * @return the instance of {@link Builder}
+         */
+        public static @NonNull Builder builder(int id, String name) {
+            return new Builder(id, name);
+        }
+
+        @Override
+        public String toString() {
+            return "MobileDevice{"
+                    + "mId=" + mId
+                    + ", mName='" + mName + '\''
+                    + ", mAvailableTransports=" + Arrays.toString(mAvailableTransports)
+                    + ", mProjecting=" + mProjecting
+                    + (mExtras != null ? ", (has extras)" : "")
+                    + '}';
+        }
+
+        /**
+         * Builder class for {@link MobileDevice}
+         */
+        public static final class Builder {
+            private int mId;
+            private String mName;
+            private IntArray mAvailableTransports = new IntArray();
+            private boolean mProjecting;
+            private Bundle mExtras;
+
+            private Builder(int id, String name) {
+                mId = id;
+                if (name == null) {
+                    throw new IllegalArgumentException("Name of the device can't be null");
+                }
+                mName = name;
+            }
+
+            /**
+             * Add supported transport
+             *
+             * @param transport supported transport by given device, must be one of the
+             * {@code PROJECTION_TRANSPORT_*}
+             * @return this builder
+             */
+            public @NonNull Builder addTransport(@ProjectionTransport int transport) {
+                checkProjectionTransport(transport);
+                mAvailableTransports.add(transport);
+                return this;
+            }
+
+            /**
+             * Indicate whether the mobile device currently projecting or not.
+             *
+             * @param projecting {@code True} if this mobile device currently projecting
+             * @return this builder
+             */
+            public @NonNull Builder setProjecting(boolean projecting) {
+                mProjecting = projecting;
+                return this;
+            }
+
+            /**
+             * Add extra information for mobile device
+             *
+             * @param extras provides an arbitrary extra information about this mobile device
+             * @return this builder
+             */
+            public @NonNull Builder setExtras(Bundle extras) {
+                mExtras = extras;
+                return this;
+            }
+
+            /** Creates new instance of {@link MobileDevice} */
+            public @NonNull MobileDevice build() {
+                return new MobileDevice(this);
+            }
+        }
+    }
+}
diff --git a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
index 1bb3ba0..e10c280 100644
--- a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
+++ b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
@@ -18,6 +18,7 @@
 
 import static android.car.Car.PERMISSION_CAR_ENROLL_TRUST;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
@@ -172,14 +173,21 @@
     }
 
     /**
-     * Activate the newly added escrow token.
+     * Returns {@code true} if the escrow token associated with the given handle is active.
+     * <p>
+     * When a new escrow token has been added as part of the Trusted device enrollment, the client
+     * will receive {@link CarTrustAgentEnrollmentCallback#onEscrowTokenAdded(long)} and
+     * {@link CarTrustAgentEnrollmentCallback#onEscrowTokenActiveStateChanged(long, boolean)}
+     * callbacks.  This method provides a way to query for the token state at a later point of time.
      *
      * @param handle the handle corresponding to the escrow token
+     * @param uid user id associated with the token
+     * @return true if the token is active, false if not
      */
     @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
-    public void activateToken(long handle) {
+    public boolean isEscrowTokenActive(long handle, int uid) {
         try {
-            mEnrollmentService.activateToken(handle);
+            return mEnrollmentService.isEscrowTokenActive(handle, uid);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -281,17 +289,19 @@
 
     /**
      * Provides a list of enrollment handles for the given user id.
+     * <p>
      * Each enrollment handle corresponds to a trusted device for the given user.
      *
      * @param uid user id.
      * @return list of the Enrollment handles for the user id.
      */
     @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
-    public List<Integer> getEnrollmentHandlesForUser(int uid) {
+    @NonNull
+    public List<Long> getEnrollmentHandlesForUser(int uid) {
         try {
-            return Arrays.stream(
-                    mEnrollmentService.getEnrollmentHandlesForUser(uid)).boxed().collect(
-                    Collectors.toList());
+            return Arrays.stream(mEnrollmentService.getEnrollmentHandlesForUser(uid))
+                    .boxed()
+                    .collect(Collectors.toList());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -346,7 +356,6 @@
          * @param active True if token has been activated, false if not.
          */
         void onEscrowTokenActiveStateChanged(long handle, boolean active);
-
     }
 
     /**
diff --git a/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl b/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
index 0476f70..5606c06 100644
--- a/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
+++ b/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
@@ -32,9 +32,9 @@
     void initiateEnrollmentHandshake(in BluetoothDevice device);
     void enrollmentHandshakeAccepted();
     void terminateEnrollmentHandshake();
-    void activateToken(in long handle);
+    boolean isEscrowTokenActive(in long handle, int uid);
     void revokeTrust(in long handle);
-    int[] getEnrollmentHandlesForUser(in int uid);
+    long[] getEnrollmentHandlesForUser(in int uid);
     void registerEnrollmentCallback(in ICarTrustAgentEnrollmentCallback callback);
     void unregisterEnrollmentCallback(in ICarTrustAgentEnrollmentCallback callback);
     void registerBleCallback(in ICarTrustAgentBleCallback callback);
diff --git a/car-test-lib/src/android/car/testapi/CarProjectionController.java b/car-test-lib/src/android/car/testapi/CarProjectionController.java
index 91de599..c3fada6 100644
--- a/car-test-lib/src/android/car/testapi/CarProjectionController.java
+++ b/car-test-lib/src/android/car/testapi/CarProjectionController.java
@@ -16,10 +16,16 @@
 
 package android.car.testapi;
 
+import android.car.projection.ProjectionOptions;
 import android.net.wifi.WifiConfiguration;
 
 /** Controller to change behavior of {@link android.car.CarProjectionManager} */
 public interface CarProjectionController {
     /** Set WifiConfiguration for wireless projection or null to simulate failure to start AP */
     void setWifiConfiguration(WifiConfiguration wifiConfiguration);
+    /**
+     * Sets {@link ProjectionOptions} object returns by
+     * {@link android.car.CarProjectionManager#getProjectionOptions()} call
+     */
+    void setProjectionOptions(ProjectionOptions projectionOptions);
 }
diff --git a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
index 1bf15cd..bba5a04 100644
--- a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
+++ b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
@@ -21,16 +21,25 @@
 import android.car.CarProjectionManager.ProjectionAccessPointCallback;
 import android.car.ICarProjection;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.net.wifi.WifiConfiguration;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  * Fake implementation of {@link ICarProjection} interface.
  *
@@ -44,6 +53,11 @@
     private WifiConfiguration mWifiConfiguration;
     private Messenger mApMessenger;
     private IBinder mApBinder;
+    private List<ICarProjectionStatusListener> mStatusListeners = new ArrayList<>();
+    private Map<IBinder, ProjectionStatus> mProjectionStatusMap = new HashMap<>();
+    private ProjectionStatus mCurrentProjectionStatus = ProjectionStatus.builder(
+            "", ProjectionStatus.PROJECTION_STATE_INACTIVE).build();
+    private ProjectionOptions mProjectionOptions;
 
     private final ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
@@ -55,6 +69,7 @@
 
     FakeCarProjectionService(Context context) {
         mContext = context;
+        mProjectionOptions = ProjectionOptions.builder().build();
     }
 
     @Override
@@ -118,7 +133,55 @@
     }
 
     @Override
+    public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+            throws RemoteException {
+        mCurrentProjectionStatus = status;
+        mProjectionStatusMap.put(token, status);
+        notifyStatusListeners(status,
+                mStatusListeners.toArray(new ICarProjectionStatusListener[0]));
+    }
+
+    private void notifyStatusListeners(ProjectionStatus status,
+            ICarProjectionStatusListener... listeners) throws RemoteException {
+        for (ICarProjectionStatusListener listener : listeners) {
+            listener.onProjectionStatusChanged(
+                    status.getState(),
+                    status.getPackageName(),
+                    new ArrayList<>(mProjectionStatusMap.values()));
+        }
+    }
+
+    @Override
+    public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        mStatusListeners.add(listener);
+        notifyStatusListeners(mCurrentProjectionStatus, listener);
+    }
+
+    @Override
+    public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        mStatusListeners.remove(listener);
+    }
+
+    @Override
     public void setWifiConfiguration(WifiConfiguration wifiConfiguration) {
         mWifiConfiguration = wifiConfiguration;
     }
+
+    @Override
+    public Bundle getProjectionOptions() throws RemoteException {
+        return mProjectionOptions.toBundle();
+    }
+
+    @Override
+    public int[] getAvailableWifiChannels(int band) throws RemoteException {
+        return new int[] {2412 /* Channel 1 */, 5180 /* Channel 36 */};
+    }
+
+
+    @Override
+    public void setProjectionOptions(ProjectionOptions projectionOptions) {
+        mProjectionOptions = projectionOptions;
+    }
 }
diff --git a/car_product/init/init.bootstat.rc b/car_product/init/init.bootstat.rc
index 430a96b..5c5e796 100644
--- a/car_product/init/init.bootstat.rc
+++ b/car_product/init/init.bootstat.rc
@@ -3,31 +3,5 @@
 # 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.zygote=running
-    exec - root root -- /system/bin/bootstat -r zygote_running
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/styles.xml b/car_product/overlay/frameworks/base/core/res/res/values/styles.xml
index 1362b97..20d6b49 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/styles.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/styles.xml
@@ -24,7 +24,7 @@
     <!-- Override the default activity transitions. We have to do a full copy and not just inherit
          and override because we're replacing the default style across the system.
     -->
-    <style name="Animation.DeviceDefault.Activity" parent="*android:Animation.Material.Activity">
+    <style name="Animation.Activity" parent="*android:Animation.Material.Activity">
         <item name="android:activityOpenEnterAnimation">@*android:anim/fade_in</item>
         <item name="android:activityOpenExitAnimation">@*android:anim/fade_out</item>
         <item name="android:activityCloseEnterAnimation">@*android:anim/fade_in</item>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml b/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
index e2f59d8..ea83ad3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
@@ -76,6 +76,12 @@
         <item name="android:textColor">@*android:color/car_button_text_color</item>
     </style>
 
+    <style name="Widget.DeviceDefault.TextView" parent="android:Widget.Material.TextView">
+        <item name="android:ellipsize">none</item>
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item>
+    </style>
+
     <style name="Widget.DeviceDefault.Button" parent="android:Widget.Material.Button">
         <item name="android:background">@*android:drawable/car_button_background</item>
         <item name="android:layout_height">@*android:dimen/car_button_height</item>
diff --git a/evs/sampleDriver/EvsEnumerator.cpp b/evs/sampleDriver/EvsEnumerator.cpp
index 9838258..96fd067 100644
--- a/evs/sampleDriver/EvsEnumerator.cpp
+++ b/evs/sampleDriver/EvsEnumerator.cpp
@@ -35,9 +35,10 @@
 std::list<EvsEnumerator::CameraRecord>   EvsEnumerator::sCameraList;
 wp<EvsGlDisplay>                           EvsEnumerator::sActiveDisplay;
 
+// Number of trials to open the camera.
+static const unsigned int kMaxRetry = 3;
 
-EvsEnumerator::EvsEnumerator()
-: kMaxRetry(3) {
+EvsEnumerator::EvsEnumerator() {
     ALOGD("EvsEnumerator created");
 
     enumerateDevices();
@@ -80,6 +81,21 @@
 Return<void> EvsEnumerator::getCameraList(getCameraList_cb _hidl_cb)  {
     ALOGD("getCameraList");
 
+    if (sCameraList.size() < 1) {
+        // WAR: this assumes that the device has at least one compatible camera and
+        // therefore keeps trying until it succeeds to open.
+        // TODO: this is required for external USB camera so would be better to
+        // subscribe hot-plug event.
+        unsigned tries = 0;
+        ALOGI("No camera is available; enumerate devices again.");
+        while (sCameraList.size() < 1 && tries++ < kMaxRetry) {
+            enumerateDevices();
+
+            // TODO: remove this.
+            usleep(5000);
+        }
+    }
+
     const unsigned numCameras = sCameraList.size();
 
     // Build up a packed array of CameraDesc for return
@@ -104,19 +120,6 @@
 
     // Is this a recognized camera id?
     CameraRecord *pRecord = findCameraById(cameraId);
-    // WAR: this assumes that the device has at least one compatible camera and
-    // therefore keeps trying until it succeeds to open.
-    // TODO: this is required for external USB camera so would be better to
-    // subscribe hot-plug event.
-    unsigned tries = 0;
-    while (!pRecord && tries++ < kMaxRetry) {
-        ALOGI("Requested camera %s not found, enumerate again, %d", cameraId.c_str(), tries);
-        enumerateDevices();
-        pRecord = findCameraById(cameraId);
-
-        // TODO: remove this.
-        usleep(5000);
-    }
 
     // Has this camera already been instantiated by another caller?
     sp<EvsV4lCamera> pActiveCamera = pRecord->activeInstance.promote();
diff --git a/evs/sampleDriver/EvsEnumerator.h b/evs/sampleDriver/EvsEnumerator.h
index 96c2e91..e7e94d4 100644
--- a/evs/sampleDriver/EvsEnumerator.h
+++ b/evs/sampleDriver/EvsEnumerator.h
@@ -48,8 +48,6 @@
     // Implementation details
     EvsEnumerator();
 
-    const unsigned kMaxRetry;
-
 private:
     struct CameraRecord {
         CameraDesc          desc;
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 9086a4a..76c4d37 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -119,6 +119,16 @@
         android:label="@string/car_permission_label_projection"
         android:description="@string/car_permission_desc_projection" />
     <permission
+            android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"
+            android:protectionLevel="system|signature"
+            android:label="@string/car_permission_label_access_projection_status"
+            android:description="@string/car_permission_desc_access_projection_status" />
+    <permission
+            android:name="android.car.permission.BIND_PROJECTION_SERVICE"
+            android:protectionLevel="signature"
+            android:label="@string/car_permission_label_bind_projection_service"
+            android:description="@string/car_permission_desc_bind_projection_service" />
+    <permission
         android:name="android.car.permission.CAR_MOCK_VEHICLE_HAL"
         android:protectionLevel="system|signature"
         android:label="@string/car_permission_label_mock_vehicle_hal"
@@ -179,6 +189,11 @@
       android:label="@string/car_permission_label_diag_clear"
       android:description="@string/car_permission_desc_diag_clear" />
     <permission
+        android:name="android.car.permission.BIND_VMS_CLIENT"
+        android:protectionLevel="signature"
+        android:label="@string/car_permission_label_bind_vms_client"
+        android:description="@string/car_permission_desc_bind_vms_client" />
+    <permission
         android:name="android.car.permission.VMS_PUBLISHER"
         android:protectionLevel="system|signature"
         android:label="@string/car_permission_label_vms_publisher"
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 6f5975a..48f346e 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -143,4 +143,19 @@
 
     <string name="config_TetheredProjectionAccessPointSsid" translatable="false">CarAP</string>
 
+    <!-- The consent activity that must be shown for every unknown mobile device before projection
+         gets started.  The format is: com.your.package/com.your.Activity  -->
+    <string name="config_projectionConsentActivity" translatable="false"/>
+
+    <!-- Display Id where projection rendering activity needs to be shown, Specify -1 to use system
+         defaults -->
+    <integer name="config_projectionActivityDisplayId" translatable="false">-1</integer>
+
+    <!-- Bounds of the projection activity on the screen. It should be in the pixels and screen
+         coordinates in the following order: left, top, right, bottom. -->
+    <integer-array name="config_projectionActivityLaunchBounds" translatable="false"/>
+
+    <!-- UI mode for projection activity. See ProjectionOptions class for possible values. -->
+    <integer name="config_projectionUiMode" translatable="false">0</integer>
+
 </resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 0855727..f74710c 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -60,12 +60,20 @@
     <string name="car_permission_desc_radio">Access your car\u2019s 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-projection [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_projection">Allows an app to project an interface from a phone on the car\u2019s display.</string>
+    <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_access_projection_status">Access projection status</string>
+    <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_access_projection_status">Allows an app to get the status of other apps projecting to the car\u2019s display.</string>
+    <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_bind_projection_service">Bind to a projection service</string>
+    <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_bind_projection_service">Allows the holder to bind to the top-level interface of a projection service. Should never be needed for normal apps."</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-audio-settings [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_audio_settings">Car Audio Settings</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\u2019s vehicle HAL for internal
@@ -124,14 +132,19 @@
     <string name="car_permission_desc_diag_clear">Clear diagnostic data from the car</string>
 
     <!-- Permission text: apps can publish VMS data [CHAR LIMIT=NONE] -->
-    <string name="car_permission_label_vms_publisher">VMS publisher</string>
+    <string name="car_permission_label_vms_publisher">VMS Publisher</string>
     <!-- Permission text: apps can send VMS messages to the car [CHAR LIMIT=NONE] -->
-    <string name="car_permission_desc_vms_publisher">Publish vms messages</string>
+    <string name="car_permission_desc_vms_publisher">Publish VMS messages</string>
 
     <!-- Permission text: apps can subscribe to VMS data [CHAR LIMIT=NONE] -->
-    <string name="car_permission_label_vms_subscriber">VMS subscriber</string>
+    <string name="car_permission_label_vms_subscriber">VMS Subscriber</string>
     <!-- Permission text: apps can receive VMS messages from the car [CHAR LIMIT=NONE] -->
-    <string name="car_permission_desc_vms_subscriber">Subscribe to vms messages</string>
+    <string name="car_permission_desc_vms_subscriber">Subscribe to VMS messages</string>
+
+    <!-- Permission text: apps can act as VMS router core [CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_bind_vms_client">VMS Client Service</string>
+    <!-- Permission text: apps can act as VMS router core [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_bind_vms_client">Bind to VMS clients</string>
 
     <!-- Permission text: apps can monitor flash storage usage [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_storage_monitoring">Flash storage monitoring</string>
diff --git a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
index 5d7fc0e..6b2169a 100644
--- a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
+++ b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
@@ -33,18 +33,24 @@
 import android.bluetooth.BluetoothPbapClient;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
+import android.car.Car;
 import android.car.CarBluetoothManager;
+import android.car.CarNotConnectedException;
 import android.car.ICarBluetoothUserService;
 import android.car.ICarUserService;
 import android.car.drivingstate.CarUxRestrictions;
 import android.car.drivingstate.ICarUxRestrictionsChangeListener;
 import android.car.hardware.CarPropertyValue;
+import android.car.hardware.power.CarPowerManager;
+import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
 import android.hardware.automotive.vehicle.V2_0.VehicleIgnitionState;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.os.Binder;
@@ -71,10 +77,10 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
-
 /**
  * A Bluetooth Device Connection policy that is specific to the use cases of a Car.  A car's
  * bluetooth capabilities in terms of the profiles it supports and its use cases are unique.
@@ -112,12 +118,15 @@
     // The main data structure that holds on to the {profile:list of known and connectible devices}
     HashMap<Integer, BluetoothDevicesInfo> mProfileToConnectableDevicesMap;
 
-    /// TODO(vnori): fix this. b/70029056
-    private static final int NUM_SUPPORTED_PHONE_CONNECTIONS = 4; // num of HFP and PBAP connections
-    private static final int NUM_SUPPORTED_MSG_CONNECTIONS = 4; // num of MAP connections
-    private static final int NUM_SUPPORTED_MUSIC_CONNECTIONS = 1; // num of A2DP connections
-    private static final int NUM_SUPPORTED_NETWORK_CONNECTIONS = 1; // num of PAN connections
-    private Map<Integer, Integer> mNumSupportedActiveConnections;
+    // Keep a map of the maximum number of connections allowed for any profile we plan to support.
+    private static final Map<Integer, Integer> sNumSupportedActiveConnections = new HashMap<>();
+    static {
+        sNumSupportedActiveConnections.put(BluetoothProfile.HEADSET_CLIENT, 4);
+        sNumSupportedActiveConnections.put(BluetoothProfile.PBAP_CLIENT, 4);
+        sNumSupportedActiveConnections.put(BluetoothProfile.A2DP_SINK, 1);
+        sNumSupportedActiveConnections.put(BluetoothProfile.MAP_CLIENT, 4);
+        sNumSupportedActiveConnections.put(BluetoothProfile.PAN, 1);
+    }
 
     private BluetoothAutoConnectStateMachine mBluetoothAutoConnectStateMachine;
     private final BluetoothAdapter mBluetoothAdapter;
@@ -133,6 +142,63 @@
     private final CarPropertyService mCarPropertyService;
     private final CarPropertyListener mPropertyEventListener;
 
+    // Car service binder to setup listening for power manager updates
+    private final Car mCar;
+    private CarPowerManager mCarPowerManager;
+    private final CarPowerStateListener mCarPowerStateListener = new CarPowerStateListener() {
+        @Override
+        public void onStateChanged(int state, CompletableFuture<Void> future) {
+            if (DBG) Log.d(TAG, "Car power state has changed to " + state);
+
+            // ON is the state when user turned on the car (it can be either ignition or
+            // door unlock) the policy for ON is defined by OEMs and we can rely on that.
+            if (state == CarPowerManager.CarPowerStateListener.ON) {
+                Log.i(TAG, "Car is powering on. Enable Bluetooth and auto-connect to devices.");
+                if (isBluetoothPersistedOn()) {
+                    enabledBluetooth();
+                }
+                initiateConnection();
+                return;
+            }
+
+            // Since we're appearing to be off after shutdown prepare, but may stay on in idle mode,
+            // we'll turn off Bluetooth to disconnect devices and better the "off" illusion
+            if (state == CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE) {
+                Log.i(TAG, "Car is preparing for shutdown. Disable bluetooth adapter.");
+                disableBluetooth();
+
+                // Let CPMS know we're ready to shutdown. Otherwise, CPMS will get stuck for
+                // up to an hour.
+                if (future != null) {
+                    future.complete(null);
+                }
+                return;
+            }
+        }
+    };
+
+
+    private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.i(TAG, "Car is now connected, getting CarPowerManager service");
+            try {
+                mCarPowerManager = (CarPowerManager) mCar.getCarManager(Car.POWER_SERVICE);
+                mCarPowerManager.setListener(mCarPowerStateListener);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Failed to get CarPowerManager instance", e);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.i(TAG, "Car is now disconnected");
+            if (mCarPowerManager != null) {
+                mCarPowerManager.clearListener();
+            }
+        }
+    };
+
     // PerUserCarService related listeners
     private final UserServiceConnectionCallback mServiceCallback;
 
@@ -193,33 +259,6 @@
                 CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0,
                 CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_1
         );
-        // mNumSupportedActiveConnections is a HashMap of mProfilesToConnect and the number of
-        // connections each profile supports currently.
-        mNumSupportedActiveConnections = new HashMap<>(mProfilesToConnect.size());
-        for (Integer profile : mProfilesToConnect) {
-            switch (profile) {
-                case BluetoothProfile.HEADSET_CLIENT:
-                    mNumSupportedActiveConnections.put(BluetoothProfile.HEADSET_CLIENT,
-                            NUM_SUPPORTED_PHONE_CONNECTIONS);
-                    break;
-                case BluetoothProfile.PBAP_CLIENT:
-                    mNumSupportedActiveConnections.put(BluetoothProfile.PBAP_CLIENT,
-                            NUM_SUPPORTED_PHONE_CONNECTIONS);
-                    break;
-                case BluetoothProfile.A2DP_SINK:
-                    mNumSupportedActiveConnections.put(BluetoothProfile.A2DP_SINK,
-                            NUM_SUPPORTED_MUSIC_CONNECTIONS);
-                    break;
-                case BluetoothProfile.MAP_CLIENT:
-                    mNumSupportedActiveConnections.put(BluetoothProfile.MAP_CLIENT,
-                            NUM_SUPPORTED_MSG_CONNECTIONS);
-                    break;
-                case BluetoothProfile.PAN:
-                    mNumSupportedActiveConnections.put(BluetoothProfile.PAN,
-                            NUM_SUPPORTED_NETWORK_CONNECTIONS);
-                    break;
-            }
-        }
 
         // Listen to events for triggering auto connect
         mPropertyEventListener = new CarPropertyListener();
@@ -232,6 +271,10 @@
             Log.w(TAG, "No Bluetooth Adapter Available");
         }
         mFastPairProvider = new FastPairProvider(mContext);
+
+        // Connect to car
+        mCar = Car.createCar(context, mCarServiceConnection);
+        mCar.connect();
     }
 
     /**
@@ -746,7 +789,7 @@
             for (Integer profile : mProfilesToConnect) {
                 // Build the BluetoothDevicesInfo for this profile.
                 BluetoothDevicesInfo devicesInfo = new BluetoothDevicesInfo(profile,
-                        mNumSupportedActiveConnections.get(profile));
+                        sNumSupportedActiveConnections.get(profile));
                 mProfileToConnectableDevicesMap.put(profile, devicesInfo);
             }
             if (DBG) {
@@ -851,6 +894,7 @@
         writeDeviceInfoToSettings();
         cleanupUserSpecificInfo();
         closeEventListeners();
+        mCar.disconnect();
     }
 
     /**
@@ -1796,6 +1840,41 @@
     }
 
     /**
+     * Get the persisted Bluetooth state from Settings
+     */
+    private boolean isBluetoothPersistedOn() {
+        return (Settings.Global.getInt(
+                mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, -1) != 0);
+    }
+
+    /**
+     * Turn on the Bluetooth Adapter.
+     */
+    private void enabledBluetooth() {
+        if (DBG) Log.d(TAG, "Enable bluetooth adapter");
+        if (mBluetoothAdapter == null) {
+            Log.e(TAG, "Cannot enable Bluetooth adapter. The object is null.");
+            return;
+        }
+        mBluetoothAdapter.enable();
+    }
+
+    /**
+     * Turn off the Bluetooth Adapter.
+     *
+     * Tells BluetoothAdapter to shut down _without_ persisting the off state as the desired state
+     * of the Bluetooth adapter for next start up.
+     */
+    private void disableBluetooth() {
+        if (DBG) Log.d(TAG, "Disable bluetooth, do not persist state across reboot");
+        if (mBluetoothAdapter == null) {
+            Log.e(TAG, "Cannot disable Bluetooth adapter. The object is null.");
+            return;
+        }
+        mBluetoothAdapter.disable(false);
+    }
+
+    /**
      * Write the device list for all bluetooth profiles that connected.
      *
      * @return true if the write was successful, false otherwise
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index 0bec14c..8cf7c46 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -19,6 +19,7 @@
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.power.ICarPower;
 import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
 import android.os.Handler;
@@ -29,6 +30,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Log;
 
 import com.android.car.hal.PowerHalService;
@@ -75,6 +77,9 @@
     private int mNextWakeupSec = 0;
     private int mTokenValue = 1;
     private boolean mShutdownOnFinish = false;
+    private boolean mIsBooting = true;
+
+    private final CarUserManagerHelper mCarUserManagerHelper;
 
     // TODO:  Make this OEM configurable.
     private static final int SHUTDOWN_POLLING_INTERVAL_MS = 2000;
@@ -96,10 +101,12 @@
     }
 
     public CarPowerManagementService(
-            Context context, PowerHalService powerHal, SystemInterface systemInterface) {
+            Context context, PowerHalService powerHal, SystemInterface systemInterface,
+            CarUserManagerHelper carUserManagerHelper) {
         mContext = context;
         mHal = powerHal;
         mSystemInterface = systemInterface;
+        mCarUserManagerHelper = carUserManagerHelper;
     }
 
     /**
@@ -113,6 +120,7 @@
         mSystemInterface = null;
         mHandlerThread = null;
         mHandler = new PowerHandler(Looper.getMainLooper());
+        mCarUserManagerHelper = null;
     }
 
     @VisibleForTesting
@@ -186,6 +194,11 @@
         handler.handlePowerStateChange();
     }
 
+    @VisibleForTesting
+    protected void clearIsBooting() {
+        mIsBooting = false;
+    }
+
     /**
      * Initiate state change from CPMS directly.
      */
@@ -262,6 +275,17 @@
     }
 
     private void handleOn() {
+        // Do not switch user if it is booting as there can be a race with CarServiceHelperService
+        if (mIsBooting) {
+            mIsBooting = false;
+        } else {
+            int targetUserId = mCarUserManagerHelper.getInitialUser();
+            if (targetUserId != UserHandle.USER_SYSTEM
+                    && targetUserId != mCarUserManagerHelper.getCurrentForegroundUserId()) {
+                Log.i(CarLog.TAG_POWER, "Desired user changed, switching to user:" + targetUserId);
+                mCarUserManagerHelper.switchToUserId(targetUserId);
+            }
+        }
         mSystemInterface.setDisplayState(true);
         sendPowerManagerEvent(CarPowerStateListener.ON);
         mHal.sendOn();
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 582d535..9570b2a 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -18,6 +18,7 @@
 import static android.car.CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH;
 import static android.car.CarProjectionManager.PROJECTION_VOICE_SEARCH;
 import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
 import static android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_FAILURE_REASON;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
@@ -28,17 +29,25 @@
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLING;
 
 import android.annotation.Nullable;
+import android.app.ActivityOptions;
 import android.bluetooth.BluetoothDevice;
 import android.car.CarProjectionManager;
 import android.car.CarProjectionManager.ProjectionAccessPointCallback;
 import android.car.ICarProjection;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Rect;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiConfiguration.GroupCipher;
 import android.net.wifi.WifiConfiguration.KeyMgmt;
@@ -47,13 +56,16 @@
 import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
 import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
 import android.net.wifi.WifiManager.SoftApCallback;
+import android.net.wifi.WifiScanner;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -67,6 +79,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Random;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * Car projection service allows to bound to projected app to boost it prioirity.
@@ -88,17 +101,36 @@
     @GuardedBy("mLock")
     private final HashMap<IBinder, WirelessClient> mWirelessClients = new HashMap<>();
 
-    @Nullable
     @GuardedBy("mLock")
-    private LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
+    private @Nullable LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
 
-    @Nullable
+
     @GuardedBy("mLock")
-    private SoftApCallback mSoftApCallback;
+    private @Nullable SoftApCallback mSoftApCallback;
+
+    @GuardedBy("mLock")
+    private final HashMap<IBinder, ProjectionReceiverClient> mProjectionReceiverClients =
+            new HashMap<>();
 
     @Nullable
     private String mApBssid;
 
+    @GuardedBy("mLock")
+    private @Nullable WifiScanner mWifiScanner;
+
+    @GuardedBy("mLock")
+    private @ProjectionState int mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+
+    @GuardedBy("mLock")
+    private ProjectionOptions mProjectionOptions;
+
+    @GuardedBy("mLock")
+    private @Nullable String mCurrentProjectionPackage;
+
+    private final List<ICarProjectionStatusListener> mProjectionStatusListeners =
+            new CopyOnWriteArrayList<>();
+
+
     private static final int WIFI_MODE_TETHERED = 1;
     private static final int WIFI_MODE_LOCALONLY = 2;
 
@@ -139,10 +171,10 @@
     private boolean mBound;
     private Intent mRegisteredService;
 
-    CarProjectionService(Context context, CarInputService carInputService,
-            CarBluetoothService carBluetoothService) {
+    CarProjectionService(Context context, @Nullable Handler handler,
+            CarInputService carInputService, CarBluetoothService carBluetoothService) {
         mContext = context;
-        mHandler = new Handler();
+        mHandler = handler == null ? new Handler() : handler;
         mCarInputService = carInputService;
         mCarBluetoothService = carBluetoothService;
         mProjectionCallbacks = new ProjectionCallbackHolder(this);
@@ -152,6 +184,7 @@
 
     @Override
     public void registerProjectionRunner(Intent serviceIntent) {
+        ICarImpl.assertProjectionPermission(mContext);
         // We assume one active projection app running in the system at one time.
         synchronized (mLock) {
             if (serviceIntent.filterEquals(mRegisteredService) && mBound) {
@@ -168,6 +201,7 @@
 
     @Override
     public void unregisterProjectionRunner(Intent serviceIntent) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             if (!serviceIntent.filterEquals(mRegisteredService)) {
                 Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
@@ -218,6 +252,7 @@
 
     @Override
     public void registerProjectionListener(ICarProjectionCallback callback, int filter) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             ProjectionCallback info = mProjectionCallbacks.get(callback);
             if (info == null) {
@@ -232,6 +267,7 @@
 
     @Override
     public void unregisterProjectionListener(ICarProjectionCallback listener) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             mProjectionCallbacks.removeBinder(listener);
         }
@@ -241,6 +277,7 @@
     @Override
     public void startProjectionAccessPoint(final Messenger messenger, IBinder binder)
             throws RemoteException {
+        ICarImpl.assertProjectionPermission(mContext);
         //TODO: check if access point already started with the desired configuration.
         registerWirelessClient(WirelessClient.of(messenger, binder));
         startAccessPoint();
@@ -248,6 +285,7 @@
 
     @Override
     public void stopProjectionAccessPoint(IBinder token) {
+        ICarImpl.assertProjectionPermission(mContext);
         Log.i(TAG, "Received stop access point request from " + token);
 
         boolean shouldReleaseAp;
@@ -264,6 +302,35 @@
         }
     }
 
+    @Override
+    public int[] getAvailableWifiChannels(int band) {
+        ICarImpl.assertProjectionPermission(mContext);
+        WifiScanner scanner;
+        synchronized (mLock) {
+            // Lazy initialization
+            if (mWifiScanner == null) {
+                mWifiScanner = mContext.getSystemService(WifiScanner.class);
+            }
+            scanner = mWifiScanner;
+        }
+        if (scanner == null) {
+            Log.w(TAG, "Unable to get WifiScanner");
+            return new int[0];
+        }
+
+        List<Integer> channels = scanner.getAvailableChannels(band);
+        if (channels == null || channels.isEmpty()) {
+            Log.w(TAG, "WifiScanner reported no available channels");
+            return new int[0];
+        }
+
+        int[] array = new int[channels.size()];
+        for (int i = 0; i < channels.size(); i++) {
+            array[i] = channels.get(i);
+        }
+        return array;
+    }
+
     /**
      * Request to disconnect the given profile on the given device, and prevent it from reconnecting
      * until either the request is released, or the process owning the given token dies.
@@ -281,6 +348,7 @@
             Log.d(TAG, "requestBluetoothProfileInhibit device=" + device + " profile=" + profile
                     + " from uid " + Binder.getCallingUid());
         }
+        ICarImpl.assertProjectionPermission(mContext);
         try {
             if (device == null) {
                 // Will be caught by AIDL and thrown to caller.
@@ -313,6 +381,7 @@
             Log.d(TAG, "releaseBluetoothProfileInhibit device=" + device + " profile=" + profile
                     + " from uid " + Binder.getCallingUid());
         }
+        ICarImpl.assertProjectionPermission(mContext);
         try {
             if (device == null) {
                 // Will be caught by AIDL and thrown to caller.
@@ -328,6 +397,155 @@
         }
     }
 
+    @Override
+    public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+            throws RemoteException {
+        if (DBG) {
+            Log.d(TAG, "updateProjectionStatus, status: " + status + ", token: " + token);
+        }
+        ICarImpl.assertProjectionPermission(mContext);
+        final String packageName = status.getPackageName();
+        final int uid = Binder.getCallingUid();
+        try {
+            if (uid != mContext.getPackageManager().getPackageUid(packageName, 0)) {
+                throw new SecurityException(
+                        "UID " + uid + " cannot update status for package " + packageName);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new SecurityException("Package " + packageName + " does not exist", e);
+        }
+
+        synchronized (mLock) {
+            ProjectionReceiverClient client = getOrCreateProjectionReceiverClientLocked(token);
+            client.mProjectionStatus = status;
+
+            if (status.isActive() || TextUtils.equals(packageName, mCurrentProjectionPackage)) {
+                mCurrentProjectionState = status.getState();
+                mCurrentProjectionPackage = packageName;
+            }
+        }
+        notifyProjectionStatusChanged(null /* notify all listeners */);
+    }
+
+    @Override
+    public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        ICarImpl.assertProjectionStatusPermission(mContext);
+        mProjectionStatusListeners.add(listener);
+
+        // Immediately notify listener with the current status.
+        notifyProjectionStatusChanged(listener);
+    }
+
+    @Override
+    public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        ICarImpl.assertProjectionStatusPermission(mContext);
+        mProjectionStatusListeners.remove(listener);
+    }
+
+    private ProjectionReceiverClient getOrCreateProjectionReceiverClientLocked(
+            IBinder token) throws RemoteException {
+        ProjectionReceiverClient client;
+        client = mProjectionReceiverClients.get(token);
+        if (client == null) {
+            client = new ProjectionReceiverClient(() -> unregisterProjectionReceiverClient(token));
+            token.linkToDeath(client.mDeathRecipient, 0 /* flags */);
+            mProjectionReceiverClients.put(token, client);
+        }
+        return client;
+    }
+
+    private void unregisterProjectionReceiverClient(IBinder token) {
+        synchronized (mLock) {
+            ProjectionReceiverClient client = mProjectionReceiverClients.remove(token);
+            if (client != null && TextUtils.equals(
+                    client.mProjectionStatus.getPackageName(), mCurrentProjectionPackage)) {
+                mCurrentProjectionPackage = null;
+                mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+            }
+        }
+    }
+
+    private void notifyProjectionStatusChanged(
+            @Nullable ICarProjectionStatusListener singleListenerToNotify)
+            throws RemoteException {
+        int currentState;
+        String currentPackage;
+        List<ProjectionStatus> statuses = new ArrayList<>();
+        synchronized (mLock) {
+            for (ProjectionReceiverClient client : mProjectionReceiverClients.values()) {
+                statuses.add(client.mProjectionStatus);
+            }
+            currentState = mCurrentProjectionState;
+            currentPackage = mCurrentProjectionPackage;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "Notify projection status change, state: " + currentState + ", pkg: "
+                    + currentPackage + ", listeners: " + mProjectionStatusListeners.size()
+                    + ", listenerToNotify: " + singleListenerToNotify);
+        }
+
+        if (singleListenerToNotify == null) {
+            for (ICarProjectionStatusListener listener : mProjectionStatusListeners) {
+                listener.onProjectionStatusChanged(currentState, currentPackage, statuses);
+            }
+        } else {
+            singleListenerToNotify.onProjectionStatusChanged(
+                    currentState, currentPackage, statuses);
+        }
+    }
+
+    @Override
+    public Bundle getProjectionOptions() {
+        ICarImpl.assertProjectionPermission(mContext);
+        synchronized (mLock) {
+            if (mProjectionOptions == null) {
+                mProjectionOptions = createProjectionOptionsBuilder()
+                        .build();
+            }
+        }
+        return mProjectionOptions.toBundle();
+    }
+
+    private ProjectionOptions.Builder createProjectionOptionsBuilder() {
+        Resources res = mContext.getResources();
+
+        ProjectionOptions.Builder builder = ProjectionOptions.builder();
+
+        ActivityOptions activityOptions = createActivityOptions(res);
+        if (activityOptions != null) {
+            builder.setProjectionActivityOptions(activityOptions);
+        }
+
+        String consentActivity = res.getString(R.string.config_projectionConsentActivity);
+        if (!TextUtils.isEmpty(consentActivity)) {
+            builder.setConsentActivity(ComponentName.unflattenFromString(consentActivity));
+        }
+
+        builder.setUiMode(res.getInteger(R.integer.config_projectionUiMode));
+        return builder;
+    }
+
+    @Nullable
+    private static ActivityOptions createActivityOptions(Resources res) {
+        ActivityOptions activityOptions = ActivityOptions.makeBasic();
+        boolean changed = false;
+        int displayId = res.getInteger(R.integer.config_projectionActivityDisplayId);
+        if (displayId != -1) {
+            activityOptions.setLaunchDisplayId(displayId);
+            changed = true;
+        }
+        int[] rawBounds = res.getIntArray(R.array.config_projectionActivityLaunchBounds);
+        if (rawBounds != null && rawBounds.length == 4) {
+            Rect bounds = new Rect(rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3]);
+            activityOptions.setLaunchBounds(bounds);
+            changed = true;
+        }
+        return changed ? activityOptions : null;
+    }
+
     private void startAccessPoint() {
         synchronized (mLock) {
             switch (mWifiMode) {
@@ -588,6 +806,10 @@
             writer.println("SoftApCallback: " + mSoftApCallback);
             writer.println("Bound to projection app: " + mBound);
             writer.println("Registered Service: " + mRegisteredService);
+            writer.println("Current projection state: " + mCurrentProjectionState);
+            writer.println("Current projection package: " + mCurrentProjectionPackage);
+            writer.println("Projection status: " + mProjectionReceiverClients);
+            writer.println("WifiScanner: " + mWifiScanner);
         }
     }
 
@@ -599,6 +821,14 @@
         }
     }
 
+    void setUiMode(Integer uiMode) {
+        synchronized (mLock) {
+            mProjectionOptions = createProjectionOptionsBuilder()
+                    .setUiMode(uiMode)
+                    .build();
+        }
+    }
+
     private static class ProjectionCallbackHolder
             extends BinderInterfaceContainer<ICarProjectionCallback> {
         ProjectionCallbackHolder(CarProjectionService service) {
@@ -796,4 +1026,21 @@
         Random random = new Random();
         return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
     }
+
+    private static class ProjectionReceiverClient {
+        private final DeathRecipient mDeathRecipient;
+        private ProjectionStatus mProjectionStatus;
+
+        ProjectionReceiverClient(DeathRecipient deathRecipient) {
+            mDeathRecipient = deathRecipient;
+        }
+
+        @Override
+        public String toString() {
+            return "ProjectionReceiverClient{"
+                    + "mDeathRecipient=" + mDeathRecipient
+                    + ", mProjectionStatus=" + mProjectionStatus
+                    + '}';
+        }
+    }
 }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 034a881..2df562e 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -44,6 +44,7 @@
 import com.android.car.systeminterface.SystemInterface;
 import com.android.car.trust.CarTrustAgentEnrollmentService;
 import com.android.car.user.CarUserService;
+import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.car.ICarServiceHelper;
@@ -90,6 +91,7 @@
     private final CarUserManagerHelper mUserManagerHelper;
     private final CarUserService mCarUserService;
     private final VmsClientManager mVmsClientManager;
+    private final VmsBrokerService mVmsBrokerService;
     private final VmsSubscriberService mVmsSubscriberService;
     private final VmsPublisherService mVmsPublisherService;
 
@@ -119,7 +121,7 @@
         mCarUserService = new CarUserService(serviceContext, mUserManagerHelper);
         mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
         mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
-                systemInterface);
+                systemInterface, mUserManagerHelper);
         mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
         mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
         mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
@@ -132,7 +134,7 @@
                 mPerUserCarServiceHelper, mCarUXRestrictionsService);
         mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
         mCarProjectionService = new CarProjectionService(
-                serviceContext, mCarInputService, mCarBluetoothService);
+                serviceContext, null /* handler */, mCarInputService, mCarBluetoothService);
         mGarageModeService = new GarageModeService(mContext);
         mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
         mCarAudioService = new CarAudioService(serviceContext);
@@ -141,10 +143,12 @@
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
                 serviceContext, mCarAudioService, this);
+        mVmsBrokerService = new VmsBrokerService();
         mVmsClientManager = new VmsClientManager(serviceContext, mUserManagerHelper);
-        mVmsSubscriberService = new VmsSubscriberService(serviceContext, mHal.getVmsHal());
-        mVmsPublisherService = new VmsPublisherService(serviceContext, mVmsClientManager,
-                mHal.getVmsHal());
+        mVmsSubscriberService = new VmsSubscriberService(
+                serviceContext, mVmsBrokerService, mHal.getVmsHal());
+        mVmsPublisherService = new VmsPublisherService(
+                serviceContext, mVmsBrokerService, mVmsClientManager, mHal.getVmsHal());
         mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
         mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
                 systemInterface);
@@ -270,7 +274,6 @@
                 assertClusterManagerPermission(mContext);
                 return mInstrumentClusterService.getManagerService();
             case Car.PROJECTION_SERVICE:
-                assertProjectionPermission(mContext);
                 return mCarProjectionService;
             case Car.VMS_SUBSCRIBER_SERVICE:
                 assertVmsSubscriberPermission(mContext);
@@ -345,6 +348,11 @@
         assertPermission(context, Car.PERMISSION_CAR_PROJECTION);
     }
 
+    /** Verify the calling context has the {@link Car#PERMISSION_CAR_PROJECTION_STATUS} */
+    public static void assertProjectionStatusPermission(Context context) {
+        assertPermission(context, Car.PERMISSION_CAR_PROJECTION_STATUS);
+    }
+
     public static void assertAnyDiagnosticPermission(Context context) {
         assertAnyPermission(context,
                 Car.PERMISSION_CAR_DIAGNOSTIC_READ_ALL,
@@ -466,6 +474,8 @@
         private static final String COMMAND_ENABLE_UXR = "enable-uxr";
         private static final String COMMAND_GARAGE_MODE = "garage-mode";
         private static final String COMMAND_GET_DO_ACTIVITIES = "get-do-activities";
+        private static final String COMMAND_GET_CARPROPERTYCONFIG = "get-carpropertyconfig";
+        private static final String COMMAND_PROJECTION_UI_MODE = "projection-ui-mode";
 
         private static final String PARAM_DAY_MODE = "day";
         private static final String PARAM_NIGHT_MODE = "night";
@@ -490,6 +500,8 @@
             pw.println("\t  Force into garage mode or check status.");
             pw.println("\tget-do-activities pkgname");
             pw.println("\t  Get Distraction Optimized activities in given package.");
+            pw.println("\tget-carpropertyconfig [propertyId]");
+            pw.println("\t  Get a CarPropertyConfig by Id in Hex or list all CarPropertyConfigs");
         }
 
         public void exec(String[] args, PrintWriter writer) {
@@ -557,6 +569,17 @@
                         }
                     }
                     break;
+                case COMMAND_GET_CARPROPERTYCONFIG:
+                    String propertyId = args.length < 2 ? "" : args[1];
+                    mHal.dumpPropertyConfigs(writer, propertyId);
+                    break;
+                case COMMAND_PROJECTION_UI_MODE:
+                    if (args.length != 2) {
+                        writer.println("Incorrect number of arguments");
+                        dumpHelp(writer);
+                        break;
+                    }
+                    mCarProjectionService.setUiMode(Integer.valueOf(args[1]));
                 default:
                     writer.println("Unknown command: \"" + arg + "\"");
                     dumpHelp(writer);
diff --git a/service/src/com/android/car/SystemActivityMonitoringService.java b/service/src/com/android/car/SystemActivityMonitoringService.java
index c2abdc6..11b6887 100644
--- a/service/src/com/android/car/SystemActivityMonitoringService.java
+++ b/service/src/com/android/car/SystemActivityMonitoringService.java
@@ -425,6 +425,10 @@
         }
 
         @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+        }
+
+        @Override
         public void onProcessDied(int pid, int uid) {
             mHandler.requestProcessDied(pid, uid);
         }
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 6bc8d3e..3c650d8 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -24,15 +24,13 @@
 import android.car.vms.VmsSubscriptionState;
 import android.content.Context;
 import android.os.Binder;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Message;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Log;
 
 import com.android.car.hal.VmsHalService;
-import com.android.car.hal.VmsHalService.VmsHalPublisherListener;
+import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
 
 import java.io.PrintWriter;
@@ -45,40 +43,40 @@
  * Binds to publishers and configures them to use this service.
  * Notifies publishers of subscription changes.
  */
-public class VmsPublisherService extends IVmsPublisherService.Stub implements CarServiceBase {
+public class VmsPublisherService extends IVmsPublisherService.Stub implements CarServiceBase,
+        VmsBrokerService.PublisherListener {
     private static final boolean DBG = true;
     private static final String TAG = "VmsPublisherService";
 
-    private static final int MSG_HAL_SUBSCRIPTION_CHANGED = 1;
-
     private final Context mContext;
     private final VmsClientManager mClientManager;
-    private final VmsListener mClientListener = new VmsListener();
+    private final VmsBrokerService mBrokerService;
     private final VmsHalService mHal;
-    private final VmsHalPublisherListener mHalPublisherListener;
+    private final VmsListener mClientListener = new VmsListener();
     private final Map<String, IVmsPublisherClient> mPublisherMap = Collections.synchronizedMap(
             new ArrayMap<>());
-    private final Handler mHandler = new EventHandler();
 
-    public VmsPublisherService(Context context, VmsClientManager clientManager, VmsHalService hal) {
+    public VmsPublisherService(Context context, VmsBrokerService brokerService,
+            VmsClientManager clientManager,
+            VmsHalService hal) {
         mContext = context;
         mClientManager = clientManager;
+        mBrokerService = brokerService;
         mHal = hal;
-        mHalPublisherListener = subscriptionState -> mHandler.sendMessage(
-                mHandler.obtainMessage(MSG_HAL_SUBSCRIPTION_CHANGED, subscriptionState));
     }
 
     @Override
     public void init() {
+        mClientListener.onClientConnected("VmsHalService", mHal.getPublisherClient());
         mClientManager.registerConnectionListener(mClientListener);
-        mHal.addPublisherListener(mHalPublisherListener);
-        mHal.signalPublisherServiceIsReady();
+        mBrokerService.addPublisherListener(this);
     }
 
     @Override
     public void release() {
+        mClientListener.onClientDisconnected("VmsHalService");
         mClientManager.unregisterConnectionListener(mClientListener);
-        mHal.removePublisherListener(mHalPublisherListener);
+        mBrokerService.removePublisherListener(this);
         mPublisherMap.clear();
     }
 
@@ -88,13 +86,12 @@
         writer.println("mPublisherMap:" + mPublisherMap.keySet());
     }
 
-    /* Called in arbitrary binder thread */
     @Override
     public void setLayersOffering(IBinder token, VmsLayersOffering offering) {
-        mHal.setPublisherLayersOffering(token, offering);
+        ICarImpl.assertVmsPublisherPermission(mContext);
+        mBrokerService.setPublisherLayersOffering(token, offering);
     }
 
-    /* Called in arbitrary binder thread */
     @Override
     public void publish(IBinder token, VmsLayer layer, int publisherId, byte[] payload) {
         if (DBG) {
@@ -104,7 +101,7 @@
 
         // Send the message to application listeners.
         Set<IVmsSubscriberClient> listeners =
-                mHal.getSubscribersForLayerFromPublisher(layer, publisherId);
+                mBrokerService.getSubscribersForLayerFromPublisher(layer, publisherId);
 
         if (DBG) {
             Log.d(TAG, "Number of subscribed apps: " + listeners.size());
@@ -116,35 +113,22 @@
                 Log.e(TAG, "unable to publish to listener: " + listener);
             }
         }
-
-        // Send the message to HAL
-        if (mHal.isHalSubscribed(layer)) {
-            Log.d(TAG, "HAL is subscribed");
-            mHal.setDataMessage(layer, payload);
-        } else {
-            Log.d(TAG, "HAL is NOT subscribed");
-        }
     }
 
-    /* Called in arbitrary binder thread */
     @Override
     public VmsSubscriptionState getSubscriptions() {
         ICarImpl.assertVmsPublisherPermission(mContext);
-        return mHal.getSubscriptionState();
+        return mBrokerService.getSubscriptionState();
     }
 
-    /* Called in arbitrary binder thread */
     @Override
     public int getPublisherId(byte[] publisherInfo) {
         ICarImpl.assertVmsPublisherPermission(mContext);
-        return mHal.getPublisherId(publisherInfo);
+        return mBrokerService.getPublisherId(publisherInfo);
     }
 
-    /**
-     * This method is only invoked by VmsHalService.notifyPublishers which is synchronized.
-     * Therefore this method only sees a non-decreasing sequence.
-     */
-    private void handleHalSubscriptionChanged(VmsSubscriptionState subscriptionState) {
+    @Override
+    public void onSubscriptionChange(VmsSubscriptionState subscriptionState) {
         // Send the message to application listeners.
         synchronized (mPublisherMap) {
             for (IVmsPublisherClient client : mPublisherMap.values()) {
@@ -183,16 +167,4 @@
             mPublisherMap.remove(publisherName);
         }
     }
-
-    private class EventHandler extends Handler {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_HAL_SUBSCRIPTION_CHANGED:
-                    handleHalSubscriptionChanged((VmsSubscriptionState) msg.obj);
-                    return;
-            }
-            super.handleMessage(msg);
-        }
-    }
 }
diff --git a/service/src/com/android/car/VmsSubscriberService.java b/service/src/com/android/car/VmsSubscriberService.java
index 61c7929..fc28978 100644
--- a/service/src/com/android/car/VmsSubscriberService.java
+++ b/service/src/com/android/car/VmsSubscriberService.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.car.hal.VmsHalService;
+import com.android.car.vms.VmsBrokerService;
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
@@ -41,14 +42,14 @@
  * + Receives HAL updates by implementing VmsHalService.VmsHalListener.
  * + Offers subscriber/publisher services by implementing IVmsService.Stub.
  */
-public class VmsSubscriberService extends IVmsSubscriberService.Stub
-        implements CarServiceBase, VmsHalService.VmsHalSubscriberListener {
+public class VmsSubscriberService extends IVmsSubscriberService.Stub implements CarServiceBase,
+        VmsBrokerService.SubscriberListener {
     private static final boolean DBG = true;
     private static final String PERMISSION = Car.PERMISSION_VMS_SUBSCRIBER;
     private static final String TAG = "VmsSubscriberService";
 
     private final Context mContext;
-    private final VmsHalService mHal;
+    private final VmsBrokerService mBrokerService;
 
     @GuardedBy("mSubscriberServiceLock")
     private final VmsSubscribersManager mSubscribersManager = new VmsSubscribersManager();
@@ -90,7 +91,7 @@
                 // Remove the subscriber subscriptions.
                 if (subscriber != null) {
                     Log.d(TAG, "Removing subscriptions for dead subscriber: " + subscriber);
-                    mHal.removeDeadSubscriber(subscriber);
+                    mBrokerService.removeDeadSubscriber(subscriber);
                 } else {
                     Log.d(TAG, "Handling dead binder with no matching subscriber");
 
@@ -195,24 +196,23 @@
         }
     }
 
-    public VmsSubscriberService(Context context, VmsHalService hal) {
+    public VmsSubscriberService(Context context, VmsBrokerService brokerService,
+            VmsHalService hal) {
         mContext = context;
-        mHal = hal;
+        mBrokerService = brokerService;
+        hal.setVmsSubscriberService(this);
     }
 
     // Implements CarServiceBase interface.
     @Override
     public void init() {
-        mHal.addSubscriberListener(this);
-
-        // Signal to subscribers that the SubscriberService is ready.
-        mHal.signalSubscriberServiceIsReady();
+        mBrokerService.addSubscriberListener(this);
     }
 
     @Override
     public void release() {
+        mBrokerService.removeSubscriberListener(this);
         mSubscribersManager.release();
-        mHal.removeSubscriberListener(this);
     }
 
     @Override
@@ -233,9 +233,6 @@
     public void removeVmsSubscriberToNotifications(IVmsSubscriberClient subscriber) {
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
-            if (mHal.containsSubscriber(subscriber)) {
-                throw new IllegalArgumentException("Subscriber has active subscriptions.");
-            }
             mSubscribersManager.remove(subscriber);
         }
     }
@@ -248,7 +245,7 @@
             mSubscribersManager.add(subscriber);
 
             // Add the subscription for the layer.
-            mHal.addSubscription(subscriber, layer);
+            mBrokerService.addSubscription(subscriber, layer);
         }
     }
 
@@ -257,32 +254,32 @@
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
             // Remove the subscription.
-            mHal.removeSubscription(subscriber, layer);
+            mBrokerService.removeSubscription(subscriber, layer);
         }
     }
 
     @Override
     public void addVmsSubscriberToPublisher(IVmsSubscriberClient subscriber,
-                                            VmsLayer layer,
-                                            int publisherId) {
+            VmsLayer layer,
+            int publisherId) {
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
             // Add the subscriber so it can subscribe.
             mSubscribersManager.add(subscriber);
 
             // Add the subscription for the layer.
-            mHal.addSubscription(subscriber, layer, publisherId);
+            mBrokerService.addSubscription(subscriber, layer, publisherId);
         }
     }
 
     @Override
     public void removeVmsSubscriberToPublisher(IVmsSubscriberClient subscriber,
-                                               VmsLayer layer,
-                                               int publisherId) {
+            VmsLayer layer,
+            int publisherId) {
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
             // Remove the subscription.
-            mHal.removeSubscription(subscriber, layer, publisherId);
+            mBrokerService.removeSubscription(subscriber, layer, publisherId);
         }
     }
 
@@ -291,7 +288,7 @@
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
             mSubscribersManager.add(subscriber);
-            mHal.addSubscription(subscriber);
+            mBrokerService.addSubscription(subscriber);
         }
     }
 
@@ -300,7 +297,7 @@
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
             // Remove the subscription.
-            mHal.removeSubscription(subscriber);
+            mBrokerService.removeSubscription(subscriber);
         }
     }
 
@@ -308,25 +305,22 @@
     public byte[] getPublisherInfo(int publisherId) {
         ICarImpl.assertVmsSubscriberPermission(mContext);
         synchronized (mSubscriberServiceLock) {
-            return mHal.getPublisherInfo(publisherId);
+            return mBrokerService.getPublisherInfo(publisherId);
         }
     }
 
     @Override
     public VmsAvailableLayers getAvailableLayers() {
-        return mHal.getAvailableLayers();
+        return mBrokerService.getAvailableLayers();
 
     }
 
-    // Implements VmsHalSubscriberListener interface
     @Override
-    public void onDataMessage(VmsLayer layer, int publisherId, byte[] payload) {
-        if (DBG) {
-            Log.d(TAG, "Publishing a message for layer: " + layer);
-        }
+    public void onMessageReceived(VmsLayer layer, int publisherId, byte[] payload) {
+        if (DBG) Log.d(TAG, "Publishing a message for layer: " + layer);
 
         Set<IVmsSubscriberClient> subscribers =
-                mHal.getSubscribersForLayerFromPublisher(layer, publisherId);
+                mBrokerService.getSubscribersForLayerFromPublisher(layer, publisherId);
 
         for (IVmsSubscriberClient subscriber : subscribers) {
             try {
@@ -340,10 +334,8 @@
     }
 
     @Override
-    public void onLayersAvaiabilityChange(VmsAvailableLayers availableLayers) {
-        if (DBG) {
-            Log.d(TAG, "Publishing layers availability change: " + availableLayers);
-        }
+    public void onLayersAvailabilityChange(VmsAvailableLayers availableLayers) {
+        if (DBG) Log.d(TAG, "Publishing layers availability change: " + availableLayers);
 
         Set<IVmsSubscriberClient> subscribers;
         subscribers = new HashSet<>(mSubscribersManager.getListeners());
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index dd38c33..ca90f78 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -397,6 +397,9 @@
         mProps.put(VehicleProperty.EV_BATTERY_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
+                Car.PERMISSION_READ_DISPLAY_UNITS,
+                Car.PERMISSION_CONTROL_DISPLAY_UNITS));
     }
 
     /**
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index f73d07c..286ca79 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -387,7 +387,7 @@
         }
     }
 
-    void set(VehiclePropValue propValue) throws PropertyTimeoutException {
+    protected void set(VehiclePropValue propValue) throws PropertyTimeoutException {
         mHalClient.setValue(propValue);
     }
 
@@ -467,33 +467,8 @@
         for (HalServiceBase service: mAllServices) {
             service.dump(writer);
         }
-
-        List<VehiclePropConfig> configList;
-        synchronized (this) {
-            configList = new ArrayList<>(mAllProperties.values());
-        }
-
-        writer.println("**All properties**");
-        for (VehiclePropConfig config : configList) {
-            StringBuilder builder = new StringBuilder()
-                    .append("Property:0x").append(toHexString(config.prop))
-                    .append(",Property name:").append(VehicleProperty.toString(config.prop))
-                    .append(",access:0x").append(toHexString(config.access))
-                    .append(",changeMode:0x").append(toHexString(config.changeMode))
-                    .append(",config:0x").append(Arrays.toString(config.configArray.toArray()))
-                    .append(",fs min:").append(config.minSampleRate)
-                    .append(",fs max:").append(config.maxSampleRate);
-            for (VehicleAreaConfig area : config.areaConfigs) {
-                builder.append(",areaId :").append(toHexString(area.areaId))
-                        .append(",f min:").append(area.minFloatValue)
-                        .append(",f max:").append(area.maxFloatValue)
-                        .append(",i min:").append(area.minInt32Value)
-                        .append(",i max:").append(area.maxInt32Value)
-                        .append(",i64 min:").append(area.minInt64Value)
-                        .append(",i64 max:").append(area.maxInt64Value);
-            }
-            writer.println(builder.toString());
-        }
+        // Dump all VHAL property configure.
+        dumpPropertyConfigs(writer, "");
         writer.println(String.format("**All Events, now ns:%d**",
                 SystemClock.elapsedRealtimeNanos()));
         for (VehiclePropertyEventInfo info : mEventLog.values()) {
@@ -510,6 +485,55 @@
     }
 
     /**
+     * Dump VHAL property configs.
+     *
+     * @param writer
+     * @param propId Property ID in Hex. If propid is empty string, dump all properties.
+     */
+    public void dumpPropertyConfigs(PrintWriter writer, String propId) {
+        List<VehiclePropConfig> configList;
+        synchronized (this) {
+            configList = new ArrayList<>(mAllProperties.values());
+        }
+
+        if (propId.equals("")) {
+            writer.println("**All properties**");
+            for (VehiclePropConfig config : configList) {
+                writer.println(dumpPropertyConfigsHelp(config));
+            }
+            return;
+        }
+        for (VehiclePropConfig config : configList) {
+            if (toHexString(config.prop).equals(propId)) {
+                writer.println(dumpPropertyConfigsHelp(config));
+                return;
+            }
+        }
+
+    }
+
+    /** Use VehiclePropertyConfig to construct string for dumping */
+    private String dumpPropertyConfigsHelp(VehiclePropConfig config) {
+        StringBuilder builder = new StringBuilder()
+                .append("Property:0x").append(toHexString(config.prop))
+                .append(",Property name:").append(VehicleProperty.toString(config.prop))
+                .append(",access:0x").append(toHexString(config.access))
+                .append(",changeMode:0x").append(toHexString(config.changeMode))
+                .append(",config:0x").append(Arrays.toString(config.configArray.toArray()))
+                .append(",fs min:").append(config.minSampleRate)
+                .append(",fs max:").append(config.maxSampleRate);
+        for (VehicleAreaConfig area : config.areaConfigs) {
+            builder.append(",areaId :").append(toHexString(area.areaId))
+                    .append(",f min:").append(area.minFloatValue)
+                    .append(",f max:").append(area.maxFloatValue)
+                    .append(",i min:").append(area.minInt32Value)
+                    .append(",i max:").append(area.maxInt32Value)
+                    .append(",i64 min:").append(area.minInt64Value)
+                    .append(",i64 max:").append(area.maxInt64Value);
+        }
+        return builder.toString();
+    }
+    /**
      * Inject a VHAL event
      *
      * @param property the Vehicle property Id as defined in the HAL
diff --git a/service/src/com/android/car/hal/VmsHalService.java b/service/src/com/android/car/hal/VmsHalService.java
index 71e1efc..1f6c2e2 100644
--- a/service/src/com/android/car/hal/VmsHalService.java
+++ b/service/src/com/android/car/hal/VmsHalService.java
@@ -19,9 +19,11 @@
 
 import static java.lang.Integer.toHexString;
 
-import android.annotation.SystemApi;
 import android.car.VehicleAreaType;
+import android.car.vms.IVmsPublisherClient;
+import android.car.vms.IVmsPublisherService;
 import android.car.vms.IVmsSubscriberClient;
+import android.car.vms.IVmsSubscriberService;
 import android.car.vms.VmsAssociatedLayer;
 import android.car.vms.VmsAvailableLayers;
 import android.car.vms.VmsLayer;
@@ -37,864 +39,751 @@
 import android.hardware.automotive.vehicle.V2_0.VmsMessageWithLayerAndPublisherIdIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageWithLayerIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsOfferingMessageIntegerValuesIndex;
-import android.os.Binder;
+import android.hardware.automotive.vehicle.V2_0.VmsPublisherInformationIntegerValuesIndex;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.car.CarLog;
-import com.android.car.VmsLayersAvailability;
-import com.android.car.VmsPublishersInfo;
-import com.android.car.VmsRouting;
-import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
- * This is a glue layer between the VehicleHal and the VmsService. It sends VMS properties back and
- * forth.
+ * VMS client implementation that proxies VmsPublisher/VmsSubscriber API calls to the Vehicle HAL
+ * using HAL-specific message encodings.
+ *
+ * @see android.hardware.automotive.vehicle.V2_0
  */
-@SystemApi
 public class VmsHalService extends HalServiceBase {
-
     private static final boolean DBG = true;
-    private static final int HAL_PROPERTY_ID = VehicleProperty.VEHICLE_MAP_SERVICE;
     private static final String TAG = "VmsHalService";
+    private static final int HAL_PROPERTY_ID = VehicleProperty.VEHICLE_MAP_SERVICE;
+    private static final int NUM_INTEGERS_IN_VMS_LAYER = 3;
 
-    private final static List<Integer> AVAILABILITY_MESSAGE_TYPES = Collections.unmodifiableList(
-            Arrays.asList(
-                    VmsMessageType.AVAILABILITY_RESPONSE,
-                    VmsMessageType.AVAILABILITY_CHANGE));
-
-    private boolean mIsSupported = false;
-    private CopyOnWriteArrayList<VmsHalPublisherListener> mPublisherListeners =
-            new CopyOnWriteArrayList<>();
-    private CopyOnWriteArrayList<VmsHalSubscriberListener> mSubscriberListeners =
-            new CopyOnWriteArrayList<>();
-
-    private final IBinder mHalPublisherToken = new Binder();
     private final VehicleHal mVehicleHal;
+    private volatile boolean mIsSupported = false;
 
-    private final Object mLock = new Object();
-    private final VmsRouting mRouting = new VmsRouting();
-    @GuardedBy("mLock")
-    private final Map<IBinder, Map<Integer, VmsLayersOffering>> mOfferings = new HashMap<>();
-    @GuardedBy("mLock")
-    private final VmsLayersAvailability mAvailableLayers = new VmsLayersAvailability();
-    private final VmsPublishersInfo mPublishersInfo = new VmsPublishersInfo();
+    private IBinder mPublisherToken;
+    private IVmsPublisherService mPublisherService;
+    private IVmsSubscriberService mSubscriberService;
 
-    /**
-     * The VmsPublisherService implements this interface to receive data from the HAL.
-     */
-    public interface VmsHalPublisherListener {
-        void onChange(VmsSubscriptionState subscriptionState);
-    }
+    @GuardedBy("this")
+    private HandlerThread mHandlerThread;
+    @GuardedBy("this")
+    private Handler mHandler;
 
-    /**
-     * The VmsSubscriberService implements this interface to receive data from the HAL.
-     */
-    public interface VmsHalSubscriberListener {
-        // Notifies the listener on a data Message from a publisher.
-        void onDataMessage(VmsLayer layer, int publisherId, byte[] payload);
+    private int mSubscriptionStateSequence = -1;
+    private int mAvailableLayersSequence = -1;
 
-        // Notifies the listener on a change in available layers.
-        void onLayersAvaiabilityChange(VmsAvailableLayers availableLayers);
-    }
-
-    /**
-     * The VmsService implements this interface to receive data from the HAL.
-     */
-    protected VmsHalService(VehicleHal vehicleHal) {
-        mVehicleHal = vehicleHal;
-        if (DBG) {
-            Log.d(TAG, "Started VmsHalService!");
+    private final IVmsPublisherClient.Stub mPublisherClient = new IVmsPublisherClient.Stub() {
+        @Override
+        public void setVmsPublisherService(IBinder token, IVmsPublisherService service) {
+            mPublisherToken = token;
+            mPublisherService = service;
         }
-    }
 
-    /**
-     * VMS subscribers should wait for a layers availability message which indicates
-     * the subscriber service is ready to handle subscription requests.
-     */
-    public void signalSubscriberServiceIsReady() {
-        notifyOfAvailabilityChange();
-    }
-
-    /**
-     * VMS publishers should wait for a subscription state message which indicates
-     * the publisher service is ready to handle offerings and publishing.
-     */
-    public void signalPublisherServiceIsReady() {
-        notifyOfSubscriptionChange();
-    }
-
-    public void addPublisherListener(VmsHalPublisherListener listener) {
-        mPublisherListeners.add(listener);
-    }
-
-    public void addSubscriberListener(VmsHalSubscriberListener listener) {
-        mSubscriberListeners.add(listener);
-    }
-
-    public void removePublisherListener(VmsHalPublisherListener listener) {
-        mPublisherListeners.remove(listener);
-    }
-
-    public void removeSubscriberListener(VmsHalSubscriberListener listener) {
-        mSubscriberListeners.remove(listener);
-    }
-
-    public void addSubscription(IVmsSubscriberClient listener, VmsLayer layer) {
-        boolean firstSubscriptionForLayer = false;
-        if (DBG) {
-            Log.d(TAG, "Checking for first subscription. Layer: " + layer);
+        @Override
+        public void onVmsSubscriptionChange(VmsSubscriptionState subscriptionState) {
+            // Registration of this callback is handled by VmsPublisherService.
+            // As a result, HAL support must be checked whenever the callback is triggered.
+            if (!mIsSupported) {
+                return;
+            }
+            if (DBG) Log.d(TAG, "Handling a subscription state change");
+            Message.obtain(mHandler, VmsMessageType.SUBSCRIPTIONS_CHANGE, subscriptionState)
+                    .sendToTarget();
         }
-        synchronized (mLock) {
-            // Check if publishers need to be notified about this change in subscriptions.
-            firstSubscriptionForLayer = !mRouting.hasLayerSubscriptions(layer);
+    };
 
-            // Add the listeners subscription to the layer
-            mRouting.addSubscription(listener, layer);
+    private final IVmsSubscriberClient.Stub mSubscriberClient = new IVmsSubscriberClient.Stub() {
+        @Override
+        public void onVmsMessageReceived(VmsLayer layer, byte[] payload) {
+            if (DBG) Log.d(TAG, "Handling a data message for Layer: " + layer);
+            // TODO(b/124130256): Set publisher ID of data message
+            Message.obtain(mHandler, VmsMessageType.DATA, createDataMessage(layer, 0, payload))
+                    .sendToTarget();
         }
-        if (firstSubscriptionForLayer) {
-            notifyHalPublishers(layer, true);
-            notifyOfSubscriptionChange();
-        }
-    }
 
-    public void removeSubscription(IVmsSubscriberClient listener, VmsLayer layer) {
-        boolean layerHasSubscribers = true;
-        synchronized (mLock) {
-            if (!mRouting.hasLayerSubscriptions(layer)) {
-                if (DBG) {
-                    Log.d(TAG, "Trying to remove a layer with no subscription: " + layer);
+        @Override
+        public void onLayersAvailabilityChanged(VmsAvailableLayers availableLayers) {
+            if (DBG) Log.d(TAG, "Handling a layer availability change");
+            Message.obtain(mHandler, VmsMessageType.AVAILABILITY_CHANGE, availableLayers)
+                    .sendToTarget();
+        }
+    };
+
+    private final Handler.Callback mHandlerCallback = msg -> {
+        int messageType = msg.what;
+        VehiclePropValue vehicleProp = null;
+        switch (messageType) {
+            case VmsMessageType.DATA:
+                vehicleProp = (VehiclePropValue) msg.obj;
+                break;
+            case VmsMessageType.SUBSCRIPTIONS_CHANGE:
+                VmsSubscriptionState subscriptionState = (VmsSubscriptionState) msg.obj;
+                // Drop out-of-order notifications
+                if (subscriptionState.getSequenceNumber() <= mSubscriptionStateSequence) {
+                    break;
                 }
-                return;
+                vehicleProp = createSubscriptionStateMessage(
+                        VmsMessageType.SUBSCRIPTIONS_CHANGE,
+                        subscriptionState);
+                mSubscriptionStateSequence = subscriptionState.getSequenceNumber();
+                break;
+            case VmsMessageType.AVAILABILITY_CHANGE:
+                VmsAvailableLayers availableLayers = (VmsAvailableLayers) msg.obj;
+                // Drop out-of-order notifications
+                if (availableLayers.getSequence() <= mAvailableLayersSequence) {
+                    break;
+                }
+                vehicleProp = createAvailableLayersMessage(
+                        VmsMessageType.AVAILABILITY_CHANGE,
+                        availableLayers);
+                mAvailableLayersSequence = availableLayers.getSequence();
+                break;
+            default:
+                Log.e(TAG, "Unexpected message type: " + messageType);
+        }
+        if (vehicleProp != null) {
+            if (DBG) Log.d(TAG, "Sending " + VmsMessageType.toString(messageType) + " message");
+            try {
+                setPropertyValue(vehicleProp);
+            } catch (RemoteException e) {
+                Log.e(TAG, "While sending " + VmsMessageType.toString(messageType));
             }
-
-            // Remove the listeners subscription to the layer
-            mRouting.removeSubscription(listener, layer);
-
-            // Check if publishers need to be notified about this change in subscriptions.
-            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer);
         }
-        if (!layerHasSubscribers) {
-            notifyHalPublishers(layer, false);
-            notifyOfSubscriptionChange();
-        }
-    }
+        return true;
+    };
 
-    public void addSubscription(IVmsSubscriberClient listener) {
-        synchronized (mLock) {
-            mRouting.addSubscription(listener);
-        }
-    }
-
-    public void removeSubscription(IVmsSubscriberClient listener) {
-        synchronized (mLock) {
-            mRouting.removeSubscription(listener);
-        }
-    }
-
-    public void addSubscription(IVmsSubscriberClient listener, VmsLayer layer, int publisherId) {
-        boolean firstSubscriptionForLayer = false;
-        synchronized (mLock) {
-            // Check if publishers need to be notified about this change in subscriptions.
-            firstSubscriptionForLayer = !(mRouting.hasLayerSubscriptions(layer) ||
-                    mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId));
-
-            // Add the listeners subscription to the layer
-            mRouting.addSubscription(listener, layer, publisherId);
-        }
-        if (firstSubscriptionForLayer) {
-            notifyHalPublishers(layer, true);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    public void removeSubscription(IVmsSubscriberClient listener, VmsLayer layer, int publisherId) {
-        boolean layerHasSubscribers = true;
-        synchronized (mLock) {
-            if (!mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId)) {
-                Log.i(TAG, "Trying to remove a layer with no subscription: " +
-                        layer + ", publisher ID:" + publisherId);
-                return;
-            }
-
-            // Remove the listeners subscription to the layer
-            mRouting.removeSubscription(listener, layer, publisherId);
-
-            // Check if publishers need to be notified about this change in subscriptions.
-            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer) ||
-                    mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId);
-        }
-        if (!layerHasSubscribers) {
-            notifyHalPublishers(layer, false);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    public void removeDeadSubscriber(IVmsSubscriberClient listener) {
-        synchronized (mLock) {
-            mRouting.removeDeadSubscriber(listener);
-        }
-    }
-
-    public Set<IVmsSubscriberClient> getSubscribersForLayerFromPublisher(VmsLayer layer,
-                                                                         int publisherId) {
-        synchronized (mLock) {
-            return mRouting.getSubscribersForLayerFromPublisher(layer, publisherId);
-        }
-    }
-
-    public Set<IVmsSubscriberClient> getAllSubscribers() {
-        synchronized (mLock) {
-            return mRouting.getAllSubscribers();
-        }
-    }
-
-    public boolean isHalSubscribed(VmsLayer layer) {
-        synchronized (mLock) {
-            return mRouting.isHalSubscribed(layer);
-        }
-    }
-
-    public VmsSubscriptionState getSubscriptionState() {
-        synchronized (mLock) {
-            return mRouting.getSubscriptionState();
-        }
+    /**
+     * Constructor used by {@link VehicleHal}
+     */
+    VmsHalService(VehicleHal vehicleHal) {
+        mVehicleHal = vehicleHal;
     }
 
     /**
-     * Assigns an idempotent ID for publisherInfo and stores it. The idempotency in this case means
-     * that the same publisherInfo will always, within a trip of the vehicle, return the same ID.
-     * The publisherInfo should be static for a binary and should only change as part of a software
-     * update. The publisherInfo is a serialized proto message which VMS clients can interpret.
+     * Retrieves the callback message handler for use by unit tests.
      */
-    public int getPublisherId(byte[] publisherInfo) {
-        if (DBG) {
-            Log.i(TAG, "Getting publisher static ID");
-        }
-        synchronized (mLock) {
-            return mPublishersInfo.getIdForInfo(publisherInfo);
-        }
-    }
-
-    public byte[] getPublisherInfo(int publisherId) {
-        if (DBG) {
-            Log.i(TAG, "Getting information for publisher ID: " + publisherId);
-        }
-        synchronized (mLock) {
-            return mPublishersInfo.getPublisherInfo(publisherId);
-        }
-    }
-
-    private void addHalSubscription(VmsLayer layer) {
-        boolean firstSubscriptionForLayer = true;
-        synchronized (mLock) {
-            // Check if publishers need to be notified about this change in subscriptions.
-            firstSubscriptionForLayer = !mRouting.hasLayerSubscriptions(layer);
-
-            // Add the listeners subscription to the layer
-            mRouting.addHalSubscription(layer);
-        }
-        if (firstSubscriptionForLayer) {
-            notifyHalPublishers(layer, true);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    private void addHalSubscriptionToPublisher(VmsLayer layer, int publisherId) {
-        boolean firstSubscriptionForLayer = true;
-        synchronized (mLock) {
-            // Check if publishers need to be notified about this change in subscriptions.
-            firstSubscriptionForLayer = !(mRouting.hasLayerSubscriptions(layer) ||
-                    mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId));
-
-            // Add the listeners subscription to the layer
-            mRouting.addHalSubscriptionToPublisher(layer, publisherId);
-        }
-        if (firstSubscriptionForLayer) {
-            notifyHalPublishers(layer, publisherId, true);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    private void removeHalSubscription(VmsLayer layer) {
-        boolean layerHasSubscribers = true;
-        synchronized (mLock) {
-            if (!mRouting.hasLayerSubscriptions(layer)) {
-                Log.i(TAG, "Trying to remove a layer with no subscription: " + layer);
-                return;
-            }
-
-            // Remove the listeners subscription to the layer
-            mRouting.removeHalSubscription(layer);
-
-            // Check if publishers need to be notified about this change in subscriptions.
-            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer);
-        }
-        if (!layerHasSubscribers) {
-            notifyHalPublishers(layer, false);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    public void removeHalSubscriptionFromPublisher(VmsLayer layer, int publisherId) {
-        boolean layerHasSubscribers = true;
-        synchronized (mLock) {
-            if (!mRouting.hasLayerSubscriptions(layer)) {
-                Log.i(TAG, "Trying to remove a layer with no subscription: " + layer);
-                return;
-            }
-
-            // Remove the listeners subscription to the layer
-            mRouting.removeHalSubscriptionToPublisher(layer, publisherId);
-
-            // Check if publishers need to be notified about this change in subscriptions.
-            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer) ||
-                    mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId);
-        }
-        if (!layerHasSubscribers) {
-            notifyHalPublishers(layer, publisherId, false);
-            notifyOfSubscriptionChange();
-        }
-    }
-
-    public boolean containsSubscriber(IVmsSubscriberClient subscriber) {
-        synchronized (mLock) {
-            return mRouting.containsSubscriber(subscriber);
-        }
-    }
-
-    public void setPublisherLayersOffering(IBinder publisherToken, VmsLayersOffering offering) {
-        synchronized (mLock) {
-            updateOffering(publisherToken, offering);
-            VmsOperationRecorder.get().setPublisherLayersOffering(offering);
-        }
-    }
-
-    public VmsAvailableLayers getAvailableLayers() {
-        synchronized (mLock) {
-            return mAvailableLayers.getAvailableLayers();
-        }
+    @VisibleForTesting
+    Handler getHandler() {
+        return mHandler;
     }
 
     /**
-     * Notify all the publishers and the HAL on subscription changes regardless of who triggered
-     * the change.
-     *
-     * @param layer          layer which is being subscribed to or unsubscribed from.
-     * @param hasSubscribers indicates if the notification is for subscription or unsubscription.
+     * Gets the {@link IVmsPublisherClient} implementation for the HAL's publisher callback.
      */
-    private void notifyHalPublishers(VmsLayer layer, boolean hasSubscribers) {
-        // notify the HAL
-        setSubscriptionRequest(layer, hasSubscribers);
-    }
-
-    private void notifyHalPublishers(VmsLayer layer, int publisherId, boolean hasSubscribers) {
-        // notify the HAL
-        setSubscriptionToPublisherRequest(layer, publisherId, hasSubscribers);
-    }
-
-    private void notifyOfSubscriptionChange() {
-        if (DBG) {
-            Log.d(TAG, "Notifying publishers on subscriptions");
-        }
-
-        // Notify the App publishers
-        for (VmsHalPublisherListener listener : mPublisherListeners) {
-            // Besides the list of layers, also a timestamp is provided to the clients.
-            // They should ignore any notification with a timestamp that is older than the most
-            // recent timestamp they have seen.
-            listener.onChange(getSubscriptionState());
-        }
+    public IBinder getPublisherClient() {
+        return mPublisherClient.asBinder();
     }
 
     /**
-     * Notify all the subscribers and the HAL on layers availability change.
-     *
-     * @param availableLayers the layers which publishers claim they made publish.
+     * Sets a reference to the {@link IVmsSubscriberService} implementation for use by the HAL.
      */
-    private void notifyOfAvailabilityChange() {
-        if (DBG) {
-            Log.d(TAG, "Notifying subscribers on layers availability");
-        }
-
-        VmsAvailableLayers availableLayers;
-        synchronized (mLock) {
-            availableLayers = mAvailableLayers.getAvailableLayers();
-        }
-
-        // notify the HAL
-        notifyAvailabilityChangeToHal(availableLayers);
-
-        // Notify the App subscribers
-        for (VmsHalSubscriberListener listener : mSubscriberListeners) {
-            listener.onLayersAvaiabilityChange(availableLayers);
-        }
-    }
-
-    @Override
-    public void init() {
-        if (mIsSupported) {
-            mVehicleHal.subscribeProperty(this, HAL_PROPERTY_ID);
-            if (DBG) {
-                Log.d(TAG, "Initializing VmsHalService VHAL property");
-            }
-        } else {
-            if (DBG) {
-                Log.d(TAG, "VmsHalService VHAL property not supported");
-            }
-        }
-    }
-
-    @Override
-    public void release() {
-        if (DBG) {
-            Log.d(TAG, "Releasing VmsHalService");
-        }
-        if (mIsSupported) {
-            mVehicleHal.unsubscribeProperty(this, HAL_PROPERTY_ID);
-        }
-        mPublisherListeners.clear();
-        mSubscriberListeners.clear();
+    public void setVmsSubscriberService(IVmsSubscriberService service) {
+        mSubscriberService = service;
     }
 
     @Override
     public Collection<VehiclePropConfig> takeSupportedProperties(
             Collection<VehiclePropConfig> allProperties) {
-        List<VehiclePropConfig> taken = new LinkedList<>();
         for (VehiclePropConfig p : allProperties) {
             if (p.prop == HAL_PROPERTY_ID) {
-                taken.add(p);
                 mIsSupported = true;
-                if (DBG) {
-                    Log.d(TAG, "takeSupportedProperties: " + toHexString(p.prop));
-                }
-                break;
+                return Collections.singleton(p);
             }
         }
-        return taken;
+        return Collections.emptySet();
     }
 
-    /**
-     * Consumes/produces HAL messages. The format of these messages is defined in:
-     * hardware/interfaces/automotive/vehicle/2.1/types.hal
-     */
     @Override
-    public void handleHalEvents(List<VehiclePropValue> values) {
-        if (DBG) {
-            Log.d(TAG, "Handling a VMS property change");
+    public void init() {
+        if (mIsSupported) {
+            if (DBG) Log.d(TAG, "Initializing VmsHalService VHAL property");
+            mVehicleHal.subscribeProperty(this, HAL_PROPERTY_ID);
+        } else {
+            if (DBG) Log.d(TAG, "VmsHalService VHAL property not supported");
+            return; // Do not continue initialization
         }
-        for (VehiclePropValue v : values) {
-            ArrayList<Integer> vec = v.value.int32Values;
-            int messageType = vec.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
 
-            if (DBG) {
-                Log.d(TAG, "Handling VMS message type: " + messageType);
+        synchronized (this) {
+            mHandlerThread = new HandlerThread(TAG);
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper(), mHandlerCallback);
+        }
+
+        if (mSubscriberService != null) {
+            try {
+                mSubscriberService.addVmsSubscriberToNotifications(mSubscriberClient);
+            } catch (RemoteException e) {
+                Log.e(TAG, "While adding subscriber callback", e);
             }
-            switch (messageType) {
-                case VmsMessageType.DATA:
-                    handleDataEvent(vec, toByteArray(v.value.bytes));
-                    break;
-                case VmsMessageType.SUBSCRIBE:
-                    handleSubscribeEvent(vec);
-                    break;
-                case VmsMessageType.UNSUBSCRIBE:
-                    handleUnsubscribeEvent(vec);
-                    break;
-                case VmsMessageType.SUBSCRIBE_TO_PUBLISHER:
-                    handleSubscribeToPublisherEvent(vec);
-                    break;
-                case VmsMessageType.UNSUBSCRIBE_TO_PUBLISHER:
-                    handleUnsubscribeFromPublisherEvent(vec);
-                    break;
-                case VmsMessageType.OFFERING:
-                    handleOfferingEvent(vec);
-                    break;
-                case VmsMessageType.AVAILABILITY_REQUEST:
-                    handleHalAvailabilityRequestEvent();
-                    break;
-                case VmsMessageType.SUBSCRIPTIONS_REQUEST:
-                    handleSubscriptionsRequestEvent();
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unexpected message type: " + messageType);
+
+            // Publish layer availability to HAL clients (this triggers HAL client initialization)
+            try {
+                mSubscriberClient.onLayersAvailabilityChanged(
+                        mSubscriberService.getAvailableLayers());
+            } catch (RemoteException e) {
+                Log.e(TAG, "While publishing layer availability", e);
+            }
+        } else if (DBG) {
+            Log.d(TAG, "VmsSubscriberService not registered");
+        }
+    }
+
+    @Override
+    public void release() {
+        synchronized (this) {
+            if (mHandlerThread != null) {
+                mHandlerThread.quitSafely();
             }
         }
-    }
 
-    private VmsLayer parseVmsLayerFromSimpleMessageIntegerValues(List<Integer> integerValues) {
-        return new VmsLayer(integerValues.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_TYPE),
-                integerValues.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_SUBTYPE),
-                integerValues.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_VERSION));
-    }
+        mSubscriptionStateSequence = -1;
+        mAvailableLayersSequence = -1;
 
-    private VmsLayer parseVmsLayerFromDataMessageIntegerValues(List<Integer> integerValues) {
-        return parseVmsLayerFromSimpleMessageIntegerValues(integerValues);
-    }
-
-    private int parsePublisherIdFromDataMessageIntegerValues(List<Integer> integerValues) {
-        return integerValues.get(VmsMessageWithLayerAndPublisherIdIntegerValuesIndex.PUBLISHER_ID);
-    }
-
-
-    /**
-     * Data message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Layer id.
-     * <li>Layer version.
-     * <li>Layer subtype.
-     * <li>Publisher ID.
-     * <li>Payload.
-     * </ul>
-     */
-    private void handleDataEvent(List<Integer> integerValues, byte[] payload) {
-        VmsLayer vmsLayer = parseVmsLayerFromDataMessageIntegerValues(integerValues);
-        int publisherId = parsePublisherIdFromDataMessageIntegerValues(integerValues);
-        if (DBG) {
-            Log.d(TAG, "Handling a data event for Layer: " + vmsLayer);
+        if (mIsSupported) {
+            if (DBG) Log.d(TAG, "Releasing VmsHalService VHAL property");
+            mVehicleHal.unsubscribeProperty(this, HAL_PROPERTY_ID);
+        } else {
+            return;
         }
 
-        // Send the message.
-        for (VmsHalSubscriberListener listener : mSubscriberListeners) {
-            listener.onDataMessage(vmsLayer, publisherId, payload);
-        }
-    }
-
-    /**
-     * Subscribe message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Layer id.
-     * <li>Layer version.
-     * <li>Layer subtype.
-     * </ul>
-     */
-    private void handleSubscribeEvent(List<Integer> integerValues) {
-        VmsLayer vmsLayer = parseVmsLayerFromSimpleMessageIntegerValues(integerValues);
-        if (DBG) {
-            Log.d(TAG, "Handling a subscribe event for Layer: " + vmsLayer);
-        }
-        addHalSubscription(vmsLayer);
-    }
-
-    /**
-     * Subscribe message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Layer id.
-     * <li>Layer version.
-     * <li>Layer subtype.
-     * <li>Publisher ID
-     * </ul>
-     */
-    private void handleSubscribeToPublisherEvent(List<Integer> integerValues) {
-        VmsLayer vmsLayer = parseVmsLayerFromSimpleMessageIntegerValues(integerValues);
-        if (DBG) {
-            Log.d(TAG, "Handling a subscribe event for Layer: " + vmsLayer);
-        }
-        int publisherId =
-                integerValues.get(VmsMessageWithLayerAndPublisherIdIntegerValuesIndex.PUBLISHER_ID);
-        addHalSubscriptionToPublisher(vmsLayer, publisherId);
-    }
-
-    /**
-     * Unsubscribe message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Layer id.
-     * <li>Layer version.
-     * </ul>
-     */
-    private void handleUnsubscribeEvent(List<Integer> integerValues) {
-        VmsLayer vmsLayer = parseVmsLayerFromSimpleMessageIntegerValues(integerValues);
-        if (DBG) {
-            Log.d(TAG, "Handling an unsubscribe event for Layer: " + vmsLayer);
-        }
-        removeHalSubscription(vmsLayer);
-    }
-
-    /**
-     * Unsubscribe message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Layer id.
-     * <li>Layer version.
-     * </ul>
-     */
-    private void handleUnsubscribeFromPublisherEvent(List<Integer> integerValues) {
-        VmsLayer vmsLayer = parseVmsLayerFromSimpleMessageIntegerValues(integerValues);
-        int publisherId =
-                integerValues.get(VmsMessageWithLayerAndPublisherIdIntegerValuesIndex.PUBLISHER_ID);
-        if (DBG) {
-            Log.d(TAG, "Handling an unsubscribe event for Layer: " + vmsLayer);
-        }
-        removeHalSubscriptionFromPublisher(vmsLayer, publisherId);
-    }
-
-    private static int NUM_INTEGERS_IN_VMS_LAYER = 3;
-
-    private VmsLayer parseVmsLayerFromIndex(List<Integer> integerValues, int index) {
-        int layerType = integerValues.get(index++);
-        int layerSutype = integerValues.get(index++);
-        int layerVersion = integerValues.get(index++);
-        return new VmsLayer(layerType, layerSutype, layerVersion);
-    }
-
-    /**
-     * Offering message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Publisher ID.
-     * <li>Number of offerings.
-     * <li>Each offering consists of:
-     * <ul>
-     * <li>Layer id.
-     * <li>Layer version.
-     * <li>Number of layer dependencies.
-     * <li>Layer type/subtype/version.
-     * </ul>
-     * </ul>
-     */
-    private void handleOfferingEvent(List<Integer> integerValues) {
-        int publisherId = integerValues.get(VmsOfferingMessageIntegerValuesIndex.PUBLISHER_ID);
-        int numLayersDependencies =
-                integerValues.get(
-                        VmsOfferingMessageIntegerValuesIndex.NUMBER_OF_OFFERS);
-        int idx = VmsOfferingMessageIntegerValuesIndex.OFFERING_START;
-
-        Set<VmsLayerDependency> offeredLayers = new HashSet<>();
-
-        // An offering is layerId, LayerVersion, LayerType, NumDeps, <LayerId, LayerVersion> X NumDeps.
-        for (int i = 0; i < numLayersDependencies; i++) {
-            VmsLayer offeredLayer = parseVmsLayerFromIndex(integerValues, idx);
-            idx += NUM_INTEGERS_IN_VMS_LAYER;
-
-            int numDependenciesForLayer = integerValues.get(idx++);
-            if (numDependenciesForLayer == 0) {
-                offeredLayers.add(new VmsLayerDependency(offeredLayer));
-            } else {
-                Set<VmsLayer> dependencies = new HashSet<>();
-
-                for (int j = 0; j < numDependenciesForLayer; j++) {
-                    VmsLayer dependantLayer = parseVmsLayerFromIndex(integerValues, idx);
-                    idx += NUM_INTEGERS_IN_VMS_LAYER;
-                    dependencies.add(dependantLayer);
-                }
-                offeredLayers.add(new VmsLayerDependency(offeredLayer, dependencies));
+        if (mSubscriberService != null) {
+            try {
+                mSubscriberService.removeVmsSubscriberToNotifications(mSubscriberClient);
+            } catch (RemoteException e) {
+                Log.e(TAG, "While removing subscriber callback", e);
             }
         }
-        // Store the HAL offering.
-        VmsLayersOffering offering = new VmsLayersOffering(offeredLayers, publisherId);
-        synchronized (mLock) {
-            updateOffering(mHalPublisherToken, offering);
-            VmsOperationRecorder.get().setHalPublisherLayersOffering(offering);
-        }
-    }
-
-    /**
-     * Availability message format:
-     * <ul>
-     * <li>Message type.
-     * <li>Number of layers.
-     * <li>Layer type/subtype/version.
-     * </ul>
-     */
-    private void handleHalAvailabilityRequestEvent() {
-        synchronized (mLock) {
-            VmsAvailableLayers availableLayers = mAvailableLayers.getAvailableLayers();
-            VehiclePropValue vehiclePropertyValue =
-                    toAvailabilityUpdateVehiclePropValue(
-                            availableLayers,
-                            VmsMessageType.AVAILABILITY_RESPONSE);
-
-            setPropertyValue(vehiclePropertyValue);
-        }
-    }
-
-    /**
-     * VmsSubscriptionRequestFormat:
-     * <ul>
-     * <li>Message type.
-     * </ul>
-     * <p>
-     * VmsSubscriptionResponseFormat:
-     * <ul>
-     * <li>Message type.
-     * <li>Sequence number.
-     * <li>Number of layers.
-     * <li>Layer type/subtype/version.
-     * </ul>
-     */
-    private void handleSubscriptionsRequestEvent() {
-        VmsSubscriptionState subscription = getSubscriptionState();
-        VehiclePropValue vehicleProp =
-                toTypedVmsVehiclePropValue(VmsMessageType.SUBSCRIPTIONS_RESPONSE);
-        VehiclePropValue.RawValue v = vehicleProp.value;
-        v.int32Values.add(subscription.getSequenceNumber());
-        Set<VmsLayer> layers = subscription.getLayers();
-        v.int32Values.add(layers.size());
-
-        //TODO(asafro): get the real number of associated layers in the subscriptions
-        //              state and send the associated layers themselves.
-        v.int32Values.add(0);
-
-        for (VmsLayer layer : layers) {
-            v.int32Values.add(layer.getType());
-            v.int32Values.add(layer.getSubtype());
-            v.int32Values.add(layer.getVersion());
-        }
-        setPropertyValue(vehicleProp);
-    }
-
-    private void updateOffering(IBinder publisherToken, VmsLayersOffering offering) {
-        synchronized (mLock) {
-            Map<Integer, VmsLayersOffering> publisherOfferings = mOfferings.get(publisherToken);
-            if (publisherOfferings == null) {
-                publisherOfferings = new HashMap<>();
-                mOfferings.put(publisherToken, publisherOfferings);
-            }
-            publisherOfferings.put(offering.getPublisherId(), offering);
-
-            // Update layers availability.
-            Set<VmsLayersOffering> allPublisherOfferings = new HashSet<>();
-            for (Map<Integer, VmsLayersOffering> offerings : mOfferings.values()) {
-                allPublisherOfferings.addAll(offerings.values());
-            }
-            if (DBG) {
-                Log.d(TAG, "New layer availability: " + allPublisherOfferings);
-            }
-            mAvailableLayers.setPublishersOffering(allPublisherOfferings);
-        }
-        notifyOfAvailabilityChange();
     }
 
     @Override
     public void dump(PrintWriter writer) {
         writer.println(TAG);
         writer.println("VmsProperty " + (mIsSupported ? "" : "not") + " supported.");
+
+        writer.println(
+                "VmsPublisherService " + (mPublisherService != null ? "" : "not") + " registered.");
+        writer.println("mSubscriptionStateSequence: " + mSubscriptionStateSequence);
+
+        writer.println("VmsSubscriberService " + (mSubscriberService != null ? "" : "not")
+                + " registered.");
+        writer.println("mAvailableLayersSequence: " + mAvailableLayersSequence);
     }
 
     /**
-     * Updates the VMS HAL property with the given value.
+     * Consumes/produces HAL messages.
      *
-     * @param layer          layer data to update the hal property.
-     * @param hasSubscribers if it is a subscribe or unsubscribe message.
-     * @return true if the call to the HAL to update the property was successful.
+     * The format of these messages is defined in:
+     * hardware/interfaces/automotive/vehicle/2.0/types.hal
      */
-    public boolean setSubscriptionRequest(VmsLayer layer, boolean hasSubscribers) {
-        VehiclePropValue vehiclePropertyValue = toTypedVmsVehiclePropValueWithLayer(
-                hasSubscribers ? VmsMessageType.SUBSCRIBE : VmsMessageType.UNSUBSCRIBE, layer);
-        return setPropertyValue(vehiclePropertyValue);
-    }
+    @Override
+    public void handleHalEvents(List<VehiclePropValue> values) {
+        if (DBG) Log.d(TAG, "Handling a VMS property change");
+        for (VehiclePropValue v : values) {
+            ArrayList<Integer> vec = v.value.int32Values;
+            int messageType = vec.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
 
-    public boolean setSubscriptionToPublisherRequest(VmsLayer layer,
-                                                     int publisherId,
-                                                     boolean hasSubscribers) {
-        VehiclePropValue vehiclePropertyValue = toTypedVmsVehiclePropValueWithLayer(
-                hasSubscribers ?
-                        VmsMessageType.SUBSCRIBE_TO_PUBLISHER :
-                        VmsMessageType.UNSUBSCRIBE_TO_PUBLISHER, layer);
-        vehiclePropertyValue.value.int32Values.add(publisherId);
-        return setPropertyValue(vehiclePropertyValue);
-    }
-
-    public boolean setDataMessage(VmsLayer layer, byte[] payload) {
-        VehiclePropValue vehiclePropertyValue =
-                toTypedVmsVehiclePropValueWithLayer(VmsMessageType.DATA, layer);
-        VehiclePropValue.RawValue v = vehiclePropertyValue.value;
-        v.bytes.ensureCapacity(payload.length);
-        for (byte b : payload) {
-            v.bytes.add(b);
-        }
-        return setPropertyValue(vehiclePropertyValue);
-    }
-
-    public boolean notifyAvailabilityChangeToHal(VmsAvailableLayers availableLayers) {
-        VehiclePropValue vehiclePropertyValue =
-                toAvailabilityUpdateVehiclePropValue(
-                        availableLayers,
-                        VmsMessageType.AVAILABILITY_CHANGE);
-
-        return setPropertyValue(vehiclePropertyValue);
-    }
-
-    private boolean setPropertyValue(VehiclePropValue vehiclePropertyValue) {
-        if (mIsSupported) {
+            if (DBG) Log.d(TAG, "Received " + VmsMessageType.toString(messageType) + " message");
             try {
-                mVehicleHal.set(vehiclePropertyValue);
-                return true;
-            } catch (PropertyTimeoutException e) {
-                Log.e(CarLog.TAG_PROPERTY,
-                        "set, property not ready 0x" + toHexString(HAL_PROPERTY_ID));
+                switch (messageType) {
+                    case VmsMessageType.DATA:
+                        handleDataEvent(vec, toByteArray(v.value.bytes));
+                        break;
+                    case VmsMessageType.SUBSCRIBE:
+                        handleSubscribeEvent(vec);
+                        break;
+                    case VmsMessageType.UNSUBSCRIBE:
+                        handleUnsubscribeEvent(vec);
+                        break;
+                    case VmsMessageType.SUBSCRIBE_TO_PUBLISHER:
+                        handleSubscribeToPublisherEvent(vec);
+                        break;
+                    case VmsMessageType.UNSUBSCRIBE_TO_PUBLISHER:
+                        handleUnsubscribeFromPublisherEvent(vec);
+                        break;
+                    case VmsMessageType.PUBLISHER_ID_REQUEST:
+                        handlePublisherIdRequest(toByteArray(v.value.bytes));
+                        break;
+                    case VmsMessageType.PUBLISHER_INFORMATION_REQUEST:
+                        handlePublisherInfoRequest(vec);
+                    case VmsMessageType.OFFERING:
+                        handleOfferingEvent(vec);
+                        break;
+                    case VmsMessageType.AVAILABILITY_REQUEST:
+                        handleAvailabilityRequestEvent();
+                        break;
+                    case VmsMessageType.SUBSCRIPTIONS_REQUEST:
+                        handleSubscriptionsRequestEvent();
+                        break;
+                    default:
+                        Log.e(TAG, "Unexpected message type: " + messageType);
+                }
+            } catch (IndexOutOfBoundsException | RemoteException e) {
+                Log.e(TAG, "While handling: " + messageType, e);
             }
         }
-        return false;
     }
 
-    private static VehiclePropValue toTypedVmsVehiclePropValue(int messageType) {
+    /**
+     * DATA message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * <li>Payload
+     * </ul>
+     */
+    private void handleDataEvent(List<Integer> message, byte[] payload)
+            throws RemoteException {
+        VmsLayer vmsLayer = parseVmsLayerFromMessage(message);
+        int publisherId = parsePublisherIdFromMessage(message);
+        if (DBG) {
+            Log.d(TAG,
+                    "Handling a data event for Layer: " + vmsLayer + " Publisher: " + publisherId);
+        }
+        mPublisherService.publish(mPublisherToken, vmsLayer, publisherId, payload);
+    }
+
+    /**
+     * SUBSCRIBE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     */
+    private void handleSubscribeEvent(List<Integer> message) throws RemoteException {
+        VmsLayer vmsLayer = parseVmsLayerFromMessage(message);
+        if (DBG) Log.d(TAG, "Handling a subscribe event for Layer: " + vmsLayer);
+        mSubscriberService.addVmsSubscriber(mSubscriberClient, vmsLayer);
+    }
+
+    /**
+     * SUBSCRIBE_TO_PUBLISHER message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * </ul>
+     */
+    private void handleSubscribeToPublisherEvent(List<Integer> message)
+            throws RemoteException {
+        VmsLayer vmsLayer = parseVmsLayerFromMessage(message);
+        int publisherId = parsePublisherIdFromMessage(message);
+        if (DBG) {
+            Log.d(TAG,
+                    "Handling a subscribe event for Layer: " + vmsLayer + " Publisher: "
+                            + publisherId);
+        }
+        mSubscriberService.addVmsSubscriberToPublisher(mSubscriberClient, vmsLayer, publisherId);
+    }
+
+    /**
+     * UNSUBSCRIBE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     */
+    private void handleUnsubscribeEvent(List<Integer> message) throws RemoteException {
+        VmsLayer vmsLayer = parseVmsLayerFromMessage(message);
+        if (DBG) Log.d(TAG, "Handling an unsubscribe event for Layer: " + vmsLayer);
+        mSubscriberService.removeVmsSubscriber(mSubscriberClient, vmsLayer);
+    }
+
+    /**
+     * UNSUBSCRIBE_TO_PUBLISHER message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * </ul>
+     */
+    private void handleUnsubscribeFromPublisherEvent(List<Integer> message)
+            throws RemoteException {
+        VmsLayer vmsLayer = parseVmsLayerFromMessage(message);
+        int publisherId = parsePublisherIdFromMessage(message);
+        if (DBG) {
+            Log.d(TAG, "Handling an unsubscribe event for Layer: " + vmsLayer + " Publisher: "
+                    + publisherId);
+        }
+        mSubscriberService.removeVmsSubscriberToPublisher(mSubscriberClient, vmsLayer, publisherId);
+    }
+
+    /**
+     * PUBLISHER_ID_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher info (bytes)
+     * </ul>
+     *
+     * PUBLISHER_ID_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * </ul>
+     */
+    private void handlePublisherIdRequest(byte[] payload)
+            throws RemoteException {
+        if (DBG) Log.d(TAG, "Handling a publisher id request event");
+
+        VehiclePropValue vehicleProp = createVmsMessage(VmsMessageType.PUBLISHER_ID_RESPONSE);
+        // Publisher ID
+        vehicleProp.value.int32Values.add(mPublisherService.getPublisherId(payload));
+
+        setPropertyValue(vehicleProp);
+    }
+
+
+    /**
+     * PUBLISHER_INFORMATION_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * </ul>
+     *
+     * PUBLISHER_INFORMATION_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher info (bytes)
+     * </ul>
+     */
+    private void handlePublisherInfoRequest(List<Integer> message)
+            throws RemoteException {
+        if (DBG) Log.d(TAG, "Handling a publisher info request event");
+        int publisherId = message.get(VmsPublisherInformationIntegerValuesIndex.PUBLISHER_ID);
+
+        VehiclePropValue vehicleProp =
+                createVmsMessage(VmsMessageType.PUBLISHER_INFORMATION_RESPONSE);
+        // Publisher Info
+        appendBytes(vehicleProp.value.bytes, mSubscriberService.getPublisherInfo(publisherId));
+
+        setPropertyValue(vehicleProp);
+    }
+
+    /**
+     * OFFERING message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * <li>Number of offerings.
+     * <li>Offerings (x number of offerings)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of layer dependencies.
+     * <li>Layer dependencies (x number of layer dependencies)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     * </ul>
+     * </ul>
+     */
+    private void handleOfferingEvent(List<Integer> message) throws RemoteException {
+        // Publisher ID for OFFERING is stored at a different index than in other message types
+        int publisherId = message.get(VmsOfferingMessageIntegerValuesIndex.PUBLISHER_ID);
+        int numLayerDependencies =
+                message.get(
+                        VmsOfferingMessageIntegerValuesIndex.NUMBER_OF_OFFERS);
+        if (DBG) {
+            Log.d(TAG, "Handling an offering event of " + numLayerDependencies
+                    + " layers for Publisher: " + publisherId);
+        }
+
+        Set<VmsLayerDependency> offeredLayers = new ArraySet<>(numLayerDependencies);
+        int idx = VmsOfferingMessageIntegerValuesIndex.OFFERING_START;
+        for (int i = 0; i < numLayerDependencies; i++) {
+            VmsLayer offeredLayer = parseVmsLayerAtIndex(message, idx);
+            idx += NUM_INTEGERS_IN_VMS_LAYER;
+
+            int numDependenciesForLayer = message.get(idx++);
+            if (numDependenciesForLayer == 0) {
+                offeredLayers.add(new VmsLayerDependency(offeredLayer));
+            } else {
+                Set<VmsLayer> dependencies = new HashSet<>();
+
+                for (int j = 0; j < numDependenciesForLayer; j++) {
+                    VmsLayer dependantLayer = parseVmsLayerAtIndex(message, idx);
+                    idx += NUM_INTEGERS_IN_VMS_LAYER;
+                    dependencies.add(dependantLayer);
+                }
+                offeredLayers.add(new VmsLayerDependency(offeredLayer, dependencies));
+            }
+        }
+
+        VmsLayersOffering offering = new VmsLayersOffering(offeredLayers, publisherId);
+        VmsOperationRecorder.get().setHalPublisherLayersOffering(offering);
+        mPublisherService.setLayersOffering(mPublisherToken, offering);
+    }
+
+    /**
+     * AVAILABILITY_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * </ul>
+     */
+    private void handleAvailabilityRequestEvent() throws RemoteException {
+        setPropertyValue(
+                createAvailableLayersMessage(VmsMessageType.AVAILABILITY_RESPONSE,
+                        mSubscriberService.getAvailableLayers()));
+    }
+
+    /**
+     * SUBSCRIPTION_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * </ul>
+     */
+    private void handleSubscriptionsRequestEvent() throws RemoteException {
+        setPropertyValue(
+                createSubscriptionStateMessage(VmsMessageType.SUBSCRIPTIONS_RESPONSE,
+                        mPublisherService.getSubscriptions()));
+    }
+
+    private void setPropertyValue(VehiclePropValue vehicleProp) throws RemoteException {
+        int messageType = vehicleProp.value.int32Values.get(
+                VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
+
+        if (!mIsSupported) {
+            Log.w(TAG, "HAL unsupported while attempting to send "
+                    + VmsMessageType.toString(messageType));
+            return;
+        }
+
+        try {
+            mVehicleHal.set(vehicleProp);
+        } catch (PropertyTimeoutException e) {
+            Log.e(CarLog.TAG_PROPERTY,
+                    "set, property not ready 0x" + toHexString(HAL_PROPERTY_ID));
+            throw new RemoteException(
+                    "Timeout while sending " + VmsMessageType.toString(messageType));
+        }
+    }
+
+    /**
+     * Creates a DATA type {@link VehiclePropValue}.
+     *
+     * DATA message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * <li>Payload
+     * </ul>
+     *
+     * @param layer Layer for which message was published.
+     */
+    private static VehiclePropValue createDataMessage(VmsLayer layer, int publisherId,
+            byte[] payload) {
+        // Message type + layer
+        VehiclePropValue vehicleProp = createVmsMessageWithLayer(VmsMessageType.DATA, layer);
+        List<Integer> message = vehicleProp.value.int32Values;
+
+        // Publisher ID
+        message.add(publisherId);
+
+        // Payload
+        appendBytes(vehicleProp.value.bytes, payload);
+        return vehicleProp;
+    }
+
+    /**
+     * Creates a SUBSCRIPTION_CHANGE or SUBSCRIPTION_RESPONSE type {@link VehiclePropValue}.
+     *
+     * Both message types have the same format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number
+     * <li>Number of layers
+     * <li>Number of associated layers
+     * <li>Layers (x number of layers) (see {@link #appendLayer})
+     * <li>Associated layers (x number of associated layers) (see {@link #appendAssociatedLayer})
+     * </ul>
+     *
+     * @param messageType       Either SUBSCRIPTIONS_CHANGE or SUBSCRIPTIONS_RESPONSE.
+     * @param subscriptionState The subscription state to encode in the message.
+     */
+    private static VehiclePropValue createSubscriptionStateMessage(int messageType,
+            VmsSubscriptionState subscriptionState) {
+        // Message type
+        VehiclePropValue vehicleProp = createVmsMessage(messageType);
+        List<Integer> message = vehicleProp.value.int32Values;
+
+        // Sequence number
+        message.add(subscriptionState.getSequenceNumber());
+
+        Set<VmsLayer> layers = subscriptionState.getLayers();
+        Set<VmsAssociatedLayer> associatedLayers = subscriptionState.getAssociatedLayers();
+
+        // Number of layers
+        message.add(layers.size());
+        // Number of associated layers
+        message.add(associatedLayers.size());
+
+        // Layers
+        for (VmsLayer layer : layers) {
+            appendLayer(message, layer);
+        }
+
+        // Associated layers
+        for (VmsAssociatedLayer layer : associatedLayers) {
+            appendAssociatedLayer(message, layer);
+        }
+        return vehicleProp;
+    }
+
+    /**
+     * Creates an AVAILABILITY_CHANGE or AVAILABILITY_RESPONSE type {@link VehiclePropValue}.
+     *
+     * Both message types have the same format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number.
+     * <li>Number of associated layers.
+     * <li>Associated layers (x number of associated layers) (see {@link #appendAssociatedLayer})
+     * </ul>
+     *
+     * @param messageType     Either AVAILABILITY_CHANGE or AVAILABILITY_RESPONSE.
+     * @param availableLayers The available layers to encode in the message.
+     */
+    private static VehiclePropValue createAvailableLayersMessage(int messageType,
+            VmsAvailableLayers availableLayers) {
+        // Message type
+        VehiclePropValue vehicleProp = createVmsMessage(messageType);
+        List<Integer> message = vehicleProp.value.int32Values;
+
+        // Sequence number
+        message.add(availableLayers.getSequence());
+
+        // Number of associated layers
+        message.add(availableLayers.getAssociatedLayers().size());
+
+        // Associated layers
+        for (VmsAssociatedLayer layer : availableLayers.getAssociatedLayers()) {
+            appendAssociatedLayer(message, layer);
+        }
+        return vehicleProp;
+    }
+
+    /**
+     * Creates a base {@link VehiclePropValue} of the requested message type, with no message fields
+     * populated.
+     *
+     * @param messageType Type of message, from {@link VmsMessageType}
+     */
+    private static VehiclePropValue createVmsMessage(int messageType) {
         VehiclePropValue vehicleProp = new VehiclePropValue();
         vehicleProp.prop = HAL_PROPERTY_ID;
         vehicleProp.areaId = VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
-        VehiclePropValue.RawValue v = vehicleProp.value;
-
-        v.int32Values.add(messageType);
+        vehicleProp.value.int32Values.add(messageType);
         return vehicleProp;
     }
 
     /**
-     * Creates a {@link VehiclePropValue}
+     * Creates a {@link VehiclePropValue} of the requested message type, with layer message fields
+     * populated. Other message fields are *not* populated.
+     *
+     * @param messageType Type of message, from {@link VmsMessageType}
+     * @param layer       Layer affected by message.
      */
-    private static VehiclePropValue toTypedVmsVehiclePropValueWithLayer(
+    private static VehiclePropValue createVmsMessageWithLayer(
             int messageType, VmsLayer layer) {
-        VehiclePropValue vehicleProp = toTypedVmsVehiclePropValue(messageType);
-        VehiclePropValue.RawValue v = vehicleProp.value;
-        v.int32Values.add(layer.getType());
-        v.int32Values.add(layer.getSubtype());
-        v.int32Values.add(layer.getVersion());
+        VehiclePropValue vehicleProp = createVmsMessage(messageType);
+        appendLayer(vehicleProp.value.int32Values, layer);
         return vehicleProp;
     }
 
-    private static VehiclePropValue toAvailabilityUpdateVehiclePropValue(
-            VmsAvailableLayers availableLayers, int messageType) {
-
-        if (!AVAILABILITY_MESSAGE_TYPES.contains(messageType)) {
-            throw new IllegalArgumentException("Unsupported availability type: " + messageType);
-        }
-        VehiclePropValue vehicleProp =
-                toTypedVmsVehiclePropValue(messageType);
-        populateAvailabilityPropValueFields(availableLayers, vehicleProp);
-        return vehicleProp;
-
+    /**
+     * Appends a {@link VmsLayer} to an encoded VMS message.
+     *
+     * Layer format:
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     *
+     * @param message Message to append to.
+     * @param layer   Layer to append.
+     */
+    private static void appendLayer(List<Integer> message, VmsLayer layer) {
+        message.add(layer.getType());
+        message.add(layer.getSubtype());
+        message.add(layer.getVersion());
     }
 
-    private static void populateAvailabilityPropValueFields(
-            VmsAvailableLayers availableAssociatedLayers,
-            VehiclePropValue vehicleProp) {
-        VehiclePropValue.RawValue v = vehicleProp.value;
-        v.int32Values.add(availableAssociatedLayers.getSequence());
-        int numLayers = availableAssociatedLayers.getAssociatedLayers().size();
-        v.int32Values.add(numLayers);
-        for (VmsAssociatedLayer layer : availableAssociatedLayers.getAssociatedLayers()) {
-            v.int32Values.add(layer.getVmsLayer().getType());
-            v.int32Values.add(layer.getVmsLayer().getSubtype());
-            v.int32Values.add(layer.getVmsLayer().getVersion());
-            v.int32Values.add(layer.getPublisherIds().size());
-            for (int publisherId : layer.getPublisherIds()) {
-                v.int32Values.add(publisherId);
-            }
+    /**
+     * Appends a {@link VmsAssociatedLayer} to an encoded VMS message.
+     *
+     * AssociatedLayer format:
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of publishers
+     * <li>Publisher ID (x number of publishers)
+     * </ul>
+     *
+     * @param message Message to append to.
+     * @param layer   Layer to append.
+     */
+    private static void appendAssociatedLayer(List<Integer> message, VmsAssociatedLayer layer) {
+        message.add(layer.getVmsLayer().getType());
+        message.add(layer.getVmsLayer().getSubtype());
+        message.add(layer.getVmsLayer().getVersion());
+        message.add(layer.getPublisherIds().size());
+        for (int publisherId : layer.getPublisherIds()) {
+            message.add(publisherId);
         }
     }
+
+    private static void appendBytes(ArrayList<Byte> dst, byte[] src) {
+        dst.ensureCapacity(src.length);
+        for (byte b : src) {
+            dst.add(b);
+        }
+    }
+
+    private static VmsLayer parseVmsLayerFromMessage(List<Integer> message) {
+        return parseVmsLayerAtIndex(message,
+                VmsMessageWithLayerIntegerValuesIndex.LAYER_TYPE);
+    }
+
+    private static VmsLayer parseVmsLayerAtIndex(List<Integer> message, int index) {
+        List<Integer> layerValues = message.subList(index, index + NUM_INTEGERS_IN_VMS_LAYER);
+        return new VmsLayer(layerValues.get(0), layerValues.get(1), layerValues.get(2));
+    }
+
+    private static int parsePublisherIdFromMessage(List<Integer> message) {
+        return message.get(VmsMessageWithLayerAndPublisherIdIntegerValuesIndex.PUBLISHER_ID);
+    }
 }
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index e6aeeac..cd421e7 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -86,7 +86,8 @@
     }
 
     @Override
-    public void activateToken(long handle) {
+    public boolean isEscrowTokenActive(long handle, int uid) {
+        return false;
     }
 
     @Override
@@ -94,9 +95,8 @@
     }
 
     @Override
-    public int[] getEnrollmentHandlesForUser(int uid) {
-        int[] handles = {};
-        return handles;
+    public long[] getEnrollmentHandlesForUser(int uid) {
+        return new long[0];
     }
 
     /**
diff --git a/service/src/com/android/car/vms/VmsBrokerService.java b/service/src/com/android/car/vms/VmsBrokerService.java
new file mode 100644
index 0000000..36f7fff
--- /dev/null
+++ b/service/src/com/android/car/vms/VmsBrokerService.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2019 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.vms;
+
+import android.car.vms.IVmsSubscriberClient;
+import android.car.vms.VmsAvailableLayers;
+import android.car.vms.VmsLayer;
+import android.car.vms.VmsLayersOffering;
+import android.car.vms.VmsOperationRecorder;
+import android.car.vms.VmsSubscriptionState;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.car.VmsLayersAvailability;
+import com.android.car.VmsPublishersInfo;
+import com.android.car.VmsRouting;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Broker service facilitating subscription handling and message passing between
+ * VmsPublisherService, VmsSubscriberService, and VmsHalService.
+ */
+public class VmsBrokerService {
+    private static final boolean DBG = true;
+    private static final String TAG = "VmsBrokerService";
+
+    private CopyOnWriteArrayList<PublisherListener> mPublisherListeners =
+            new CopyOnWriteArrayList<>();
+    private CopyOnWriteArrayList<SubscriberListener> mSubscriberListeners =
+            new CopyOnWriteArrayList<>();
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final VmsRouting mRouting = new VmsRouting();
+    @GuardedBy("mLock")
+    private final Map<IBinder, Map<Integer, VmsLayersOffering>> mOfferings = new HashMap<>();
+    @GuardedBy("mLock")
+    private final VmsLayersAvailability mAvailableLayers = new VmsLayersAvailability();
+    @GuardedBy("mLock")
+    private final VmsPublishersInfo mPublishersInfo = new VmsPublishersInfo();
+
+    /**
+     * The VMS publisher and HAL services implement this interface to receive publisher callbacks.
+     */
+    public interface PublisherListener {
+        /**
+         * Callback triggered when publisher subscription state changes.
+         *
+         * @param subscriptionState Current subscription state.
+         */
+        void onSubscriptionChange(VmsSubscriptionState subscriptionState);
+    }
+
+    /**
+     * The VMS publisher and HAL services implement this interface to receive subscriber callbacks.
+     */
+    public interface SubscriberListener {
+        /**
+         * Callback triggered when data is published for a given layer.
+         *
+         * @param layer       Layer data is being published for
+         * @param publisherId Publisher of data
+         * @param payload     Layer data
+         */
+        void onMessageReceived(VmsLayer layer, int publisherId, byte[] payload);
+
+        /**
+         * Callback triggered when the layers available for subscription changes.
+         *
+         * @param availableLayers Current layer availability
+         */
+        void onLayersAvailabilityChange(VmsAvailableLayers availableLayers);
+    }
+
+    /**
+     * Constructs new broker service.
+     */
+    public VmsBrokerService() {
+        if (DBG) Log.d(TAG, "Started VmsBrokerService!");
+    }
+
+    /**
+     * Adds a listener for publisher callbacks.
+     *
+     * @param listener Publisher callback listener
+     */
+    public void addPublisherListener(PublisherListener listener) {
+        mPublisherListeners.add(listener);
+    }
+
+    /**
+     * Adds a listener for subscriber callbacks.
+     *
+     * @param listener Subscriber callback listener
+     */
+    public void addSubscriberListener(SubscriberListener listener) {
+        mSubscriberListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener for publisher callbacks.
+     *
+     * @param listener Publisher callback listener
+     */
+    public void removePublisherListener(PublisherListener listener) {
+        mPublisherListeners.remove(listener);
+    }
+
+    /**
+     * Removes a listener for subscriber callbacks.
+     *
+     * @param listener Subscriber callback listener
+     */
+    public void removeSubscriberListener(SubscriberListener listener) {
+        mSubscriberListeners.remove(listener);
+    }
+
+    /**
+     * Adds a subscription to all layers.
+     *
+     * @param subscriber Subscriber client to send layer data
+     */
+    public void addSubscription(IVmsSubscriberClient subscriber) {
+        synchronized (mLock) {
+            mRouting.addSubscription(subscriber);
+        }
+    }
+
+    /**
+     * Removes a subscription to all layers.
+     *
+     * @param subscriber Subscriber client to remove subscription for
+     */
+    public void removeSubscription(IVmsSubscriberClient subscriber) {
+        synchronized (mLock) {
+            mRouting.removeSubscription(subscriber);
+        }
+    }
+
+    /**
+     * Adds a layer subscription.
+     *
+     * @param subscriber Subscriber client to send layer data
+     * @param layer      Layer to send
+     */
+    public void addSubscription(IVmsSubscriberClient subscriber, VmsLayer layer) {
+        boolean firstSubscriptionForLayer;
+        if (DBG) Log.d(TAG, "Checking for first subscription. Layer: " + layer);
+        synchronized (mLock) {
+            // Check if publishers need to be notified about this change in subscriptions.
+            firstSubscriptionForLayer = !mRouting.hasLayerSubscriptions(layer);
+
+            // Add the listeners subscription to the layer
+            mRouting.addSubscription(subscriber, layer);
+        }
+        if (firstSubscriptionForLayer) {
+            notifyOfSubscriptionChange();
+        }
+    }
+
+    /**
+     * Removes a layer subscription.
+     *
+     * @param subscriber Subscriber client to remove subscription for
+     * @param layer      Layer to remove
+     */
+    public void removeSubscription(IVmsSubscriberClient subscriber, VmsLayer layer) {
+        boolean layerHasSubscribers;
+        synchronized (mLock) {
+            if (!mRouting.hasLayerSubscriptions(layer)) {
+                if (DBG) Log.d(TAG, "Trying to remove a layer with no subscription: " + layer);
+                return;
+            }
+
+            // Remove the listeners subscription to the layer
+            mRouting.removeSubscription(subscriber, layer);
+
+            // Check if publishers need to be notified about this change in subscriptions.
+            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer);
+        }
+        if (!layerHasSubscribers) {
+            notifyOfSubscriptionChange();
+        }
+    }
+
+    /**
+     * Adds a publisher-specific layer subscription.
+     *
+     * @param subscriber  Subscriber client to send layer data
+     * @param layer       Layer to send
+     * @param publisherId Publisher of layer
+     */
+    public void addSubscription(IVmsSubscriberClient subscriber, VmsLayer layer, int publisherId) {
+        boolean firstSubscriptionForLayer;
+        synchronized (mLock) {
+            // Check if publishers need to be notified about this change in subscriptions.
+            firstSubscriptionForLayer = !(mRouting.hasLayerSubscriptions(layer)
+                    || mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId));
+
+            // Add the listeners subscription to the layer
+            mRouting.addSubscription(subscriber, layer, publisherId);
+        }
+        if (firstSubscriptionForLayer) {
+            notifyOfSubscriptionChange();
+        }
+    }
+
+    /**
+     * Removes a publisher-specific layer subscription.
+     *
+     * @param subscriber  Subscriber client to remove subscription for
+     * @param layer       Layer to remove
+     * @param publisherId Publisher of layer
+     */
+    public void removeSubscription(IVmsSubscriberClient subscriber, VmsLayer layer,
+            int publisherId) {
+        boolean layerHasSubscribers;
+        synchronized (mLock) {
+            if (!mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId)) {
+                Log.i(TAG, "Trying to remove a layer with no subscription: "
+                        + layer + ", publisher ID:" + publisherId);
+                return;
+            }
+
+            // Remove the listeners subscription to the layer
+            mRouting.removeSubscription(subscriber, layer, publisherId);
+
+            // Check if publishers need to be notified about this change in subscriptions.
+            layerHasSubscribers = mRouting.hasLayerSubscriptions(layer)
+                    || mRouting.hasLayerFromPublisherSubscriptions(layer, publisherId);
+        }
+        if (!layerHasSubscribers) {
+            notifyOfSubscriptionChange();
+        }
+    }
+
+    /**
+     * Removes a disconnected subscriber's subscriptions
+     *
+     * @param subscriber Subscriber that was disconnected
+     */
+    public void removeDeadSubscriber(IVmsSubscriberClient subscriber) {
+        synchronized (mLock) {
+            mRouting.removeDeadSubscriber(subscriber);
+        }
+    }
+
+    /**
+     * Gets all subscribers for a specific layer/publisher combination.
+     *
+     * @param layer       Layer to query
+     * @param publisherId Publisher of layer
+     */
+    public Set<IVmsSubscriberClient> getSubscribersForLayerFromPublisher(VmsLayer layer,
+            int publisherId) {
+        synchronized (mLock) {
+            return mRouting.getSubscribersForLayerFromPublisher(layer, publisherId);
+        }
+    }
+
+    /**
+     * Gets the state of all layer subscriptions.
+     */
+    public VmsSubscriptionState getSubscriptionState() {
+        synchronized (mLock) {
+            return mRouting.getSubscriptionState();
+        }
+    }
+
+    /**
+     * Assigns an idempotent ID for publisherInfo and stores it. The idempotency in this case means
+     * that the same publisherInfo will always, within a trip of the vehicle, return the same ID.
+     * The publisherInfo should be static for a binary and should only change as part of a software
+     * update. The publisherInfo is a serialized proto message which VMS clients can interpret.
+     */
+    public int getPublisherId(byte[] publisherInfo) {
+        if (DBG) Log.i(TAG, "Getting publisher static ID");
+        synchronized (mLock) {
+            return mPublishersInfo.getIdForInfo(publisherInfo);
+        }
+    }
+
+    /**
+     * Gets the publisher information data registered in {@link #getPublisherId(byte[])}
+     *
+     * @param publisherId Publisher ID to query
+     * @return Publisher information
+     */
+    public byte[] getPublisherInfo(int publisherId) {
+        if (DBG) Log.i(TAG, "Getting information for publisher ID: " + publisherId);
+        synchronized (mLock) {
+            return mPublishersInfo.getPublisherInfo(publisherId);
+        }
+    }
+
+    /**
+     * Sets the layers offered by the publisher with the given publisher token.
+     *
+     * @param publisherToken Identifier token of publisher
+     * @param offering       Layers offered by publisher
+     */
+    public void setPublisherLayersOffering(IBinder publisherToken, VmsLayersOffering offering) {
+        synchronized (mLock) {
+            Map<Integer, VmsLayersOffering> publisherOfferings = mOfferings.computeIfAbsent(
+                    publisherToken, k -> new HashMap<>());
+            publisherOfferings.put(offering.getPublisherId(), offering);
+
+            // Update layers availability.
+            Set<VmsLayersOffering> allPublisherOfferings = new HashSet<>();
+            for (Map<Integer, VmsLayersOffering> offerings : mOfferings.values()) {
+                allPublisherOfferings.addAll(offerings.values());
+            }
+            if (DBG) Log.d(TAG, "New layer availability: " + allPublisherOfferings);
+            mAvailableLayers.setPublishersOffering(allPublisherOfferings);
+        }
+        VmsOperationRecorder.get().setPublisherLayersOffering(offering);
+        notifyOfAvailabilityChange();
+    }
+
+    /**
+     * Gets all layers available for subscription.
+     *
+     * @return All available layers
+     */
+    public VmsAvailableLayers getAvailableLayers() {
+        synchronized (mLock) {
+            return mAvailableLayers.getAvailableLayers();
+        }
+    }
+
+    private void notifyOfSubscriptionChange() {
+        if (DBG) Log.d(TAG, "Notifying publishers on subscriptions");
+
+        VmsSubscriptionState subscriptionState = getSubscriptionState();
+        // Notify the App publishers
+        for (PublisherListener listener : mPublisherListeners) {
+            listener.onSubscriptionChange(subscriptionState);
+        }
+    }
+
+    private void notifyOfAvailabilityChange() {
+        if (DBG) Log.d(TAG, "Notifying subscribers on layers availability");
+
+        VmsAvailableLayers availableLayers = getAvailableLayers();
+        // Notify the App subscribers
+        for (SubscriberListener listener : mSubscriberListeners) {
+            listener.onLayersAvailabilityChange(availableLayers);
+        }
+    }
+}
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index c341b7a..0657cba 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -313,6 +313,15 @@
             mBinder = null;
         }
 
+        void rebind() {
+            unbind();
+            if (DBG) {
+                Log.d(TAG,
+                        String.format("rebinding %s after %dms", mFullName, mMillisBeforeRebind));
+            }
+            mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+        }
+
         @Override
         public void onServiceConnected(ComponentName name, IBinder binder) {
             if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
@@ -323,20 +332,13 @@
         @Override
         public void onServiceDisconnected(ComponentName name) {
             if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
-            if (mBinder != null) {
-                notifyListenersOnClientDisconnected(mFullName);
-            }
-            mBinder = null;
-            // No need to unbind and rebind, per onServiceDisconnected documentation:
-            // "binding to the service will remain active, and you will receive a call
-            // to onServiceConnected when the Service is next running"
+            rebind();
         }
 
         @Override
         public void onBindingDied(ComponentName name) {
             if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
-            unbind();
-            mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+            rebind();
         }
 
         @Override
diff --git a/tests/DirectRenderingClusterSample/res/values-en-rUS/dimens.xml b/tests/DirectRenderingClusterSample/res/values-en-rUS/dimens.xml
new file mode 100644
index 0000000..05eef22
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values-en-rUS/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!--                                   -->
+    <!-- Sensor value conversion constants -->
+    <!--                                   -->
+    <!-- Speed: meters per second to miles per hour -->
+    <item name="speed_factor" format="float" type="dimen">2.236936</item>
+    <!-- Distance: miles to meters -->
+    <item name="distance_factor" format="float" type="dimen">1609.344</item>
+</resources>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/values/dimens.xml b/tests/DirectRenderingClusterSample/res/values/dimens.xml
index 2979b36..6d22a70 100644
--- a/tests/DirectRenderingClusterSample/res/values/dimens.xml
+++ b/tests/DirectRenderingClusterSample/res/values/dimens.xml
@@ -40,4 +40,12 @@
     <dimen name="laneview_height">25dp</dimen>
     <dimen name="lane_width">50dp</dimen>
     <dimen name="lane_height">50dp</dimen>
+
+    <!--                                   -->
+    <!-- Sensor value conversion constants -->
+    <!--                                   -->
+    <!-- Speed: meters per second to kilometers per hour -->
+    <item name="speed_factor" format="float" type="dimen">3.6</item>
+    <!-- Distance: kilometers to meters -->
+    <item name="distance_factor" format="float" type="dimen">1000</item>
 </resources>
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
index d102a49..6cabd69 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -60,6 +60,10 @@
         }
 
         @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServicetypes) {
+        }
+
+        @Override
         public void onProcessDied(int pid, int uid) {
             notifyTopActivities();
         }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index 84d67b9..e3ee05a 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -27,12 +27,9 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Binder;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -47,37 +44,41 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
  * virtual display that is transmitted to an external screen.
  */
-public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService {
+public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService implements
+        ImageResolver.BitmapFetcher {
     private static final String TAG = "Cluster.SampleService";
 
     private static final int NO_DISPLAY = -1;
 
+    static final int NAV_STATE_EVENT_ID = 1;
     static final String LOCAL_BINDING_ACTION = "local";
     static final String NAV_STATE_BUNDLE_KEY = "navstate";
-    static final int NAV_STATE_EVENT_ID = 1;
-    static final int MSG_SET_ACTIVITY_LAUNCH_OPTIONS = 1;
-    static final int MSG_ON_NAVIGATION_STATE_CHANGED = 2;
-    static final int MSG_ON_KEY_EVENT = 3;
-    static final int MSG_REGISTER_CLIENT = 4;
-    static final int MSG_UNREGISTER_CLIENT = 5;
-    static final String MSG_KEY_CATEGORY = "category";
-    static final String MSG_KEY_ACTIVITY_DISPLAY_ID = "activity_display_id";
-    static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
-    static final String MSG_KEY_KEY_EVENT = "key_event";
 
-    private List<Messenger> mClients = new ArrayList<>();
+    private List<ServiceClient> mClients = new ArrayList<>();
     private ClusterDisplayProvider mDisplayProvider;
     private int mDisplayId = NO_DISPLAY;
-    private final IBinder mLocalBinder = new Messenger(new MessageHandler(this)).getBinder();
+    private final IBinder mLocalBinder = new LocalBinder();
+    private final ImageResolver mImageResolver = new ImageResolver(this);
+
+    public interface ServiceClient {
+        void onKeyEvent(KeyEvent keyEvent);
+        void onNavigationStateChange(NavigationState navState);
+    }
+
+    public class LocalBinder extends Binder {
+        ClusterRenderingServiceImpl getService() {
+            return ClusterRenderingServiceImpl.this;
+        }
+    }
 
     private final DisplayListener mDisplayListener = new DisplayListener() {
         @Override
@@ -98,43 +99,33 @@
         }
     };
 
-    private static class MessageHandler extends Handler {
-        private final WeakReference<ClusterRenderingServiceImpl> mService;
-
-        MessageHandler(ClusterRenderingServiceImpl service) {
-            mService = new WeakReference<>(service);
+    public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
+        ActivityOptions options = displayId != Display.INVALID_DISPLAY
+                ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
+                : null;
+        setClusterActivityLaunchOptions(options);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
+                    options, options.getLaunchDisplayId()));
         }
-
-        @Override
-        public void handleMessage(Message msg) {
-            Log.d(TAG, "handleMessage: " + msg.what);
-            switch (msg.what) {
-                case MSG_SET_ACTIVITY_LAUNCH_OPTIONS: {
-                    int displayId = msg.getData().getInt(MSG_KEY_ACTIVITY_DISPLAY_ID);
-                    Bundle state = msg.getData().getBundle(MSG_KEY_ACTIVITY_STATE);
-                    String category = msg.getData().getString(MSG_KEY_CATEGORY);
-                    ActivityOptions options = displayId != Display.INVALID_DISPLAY
-                            ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
-                            : null;
-                    mService.get().setClusterActivityLaunchOptions(category, options);
-                    Log.d(TAG, String.format("activity options set: %s = %s (displayeId: %d)",
-                            category, options, options.getLaunchDisplayId()));
-                    mService.get().setClusterActivityState(category, state);
-                    Log.d(TAG, String.format("activity state set: %s = %s", category, state));
-                    break;
-                }
-                case MSG_REGISTER_CLIENT:
-                    mService.get().mClients.add(msg.replyTo);
-                    break;
-                case MSG_UNREGISTER_CLIENT:
-                    mService.get().mClients.remove(msg.replyTo);
-                    break;
-                default:
-                    super.handleMessage(msg);
-            }
+        setClusterActivityState(state);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("activity state set: %s", state));
         }
     }
 
+    public void registerClient(ServiceClient client) {
+        mClients.add(client);
+    }
+
+    public void unregisterClient(ServiceClient client) {
+        mClients.remove(client);
+    }
+
+    public ImageResolver getImageResolver() {
+        return mImageResolver;
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         Log.d(TAG, "onBind, intent: " + intent);
@@ -161,30 +152,20 @@
 
     @Override
     public void onKeyEvent(KeyEvent keyEvent) {
-        Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
-        Bundle data = new Bundle();
-        data.putParcelable(MSG_KEY_KEY_EVENT, keyEvent);
-        broadcastClientMessage(MSG_ON_KEY_EVENT, data);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
+        }
+        broadcastClientEvent(client -> client.onKeyEvent(keyEvent));
     }
 
     /**
-     * Broadcasts a message to all the registered service clients
+     * Broadcasts an event to all the registered service clients
      *
-     * @param what event identifier
-     * @param data event data
+     * @param event event to broadcast
      */
-    private void broadcastClientMessage(int what, Bundle data) {
-        Log.d(TAG, "broadcast message " + what + " to " + mClients.size() + " clients");
-        for (int i = mClients.size() - 1; i >= 0; i--) {
-            Messenger client = mClients.get(i);
-            try {
-                Message msg = Message.obtain(null, what);
-                msg.setData(data);
-                client.send(msg);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Client " + i + " is dead", ex);
-                mClients.remove(i);
-            }
+    private void broadcastClientEvent(Consumer<ServiceClient> event) {
+        for (ServiceClient client : mClients) {
+            event.accept(client);
         }
     }
 
@@ -210,7 +191,7 @@
                         bundleSummary.append(navState.toString());
 
                         // Update clients
-                        broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
+                        broadcastClientEvent(client -> client.onNavigationStateChange(navState));
                     } else {
                         for (String key : bundle.keySet()) {
                             bundleSummary.append(key);
@@ -222,9 +203,8 @@
                     Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
                 } catch (Exception e) {
                     Log.e(TAG, "Error parsing event data (" + eventType + ", " + bundle + ")", e);
-                    bundle.putParcelable(NAV_STATE_BUNDLE_KEY, new NavigationState.Builder().build()
-                            .toParcelable());
-                    broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
+                    NavigationState navState = new NavigationState.Builder().build();
+                    broadcastClientEvent(client -> client.onNavigationStateChange(navState));
                 }
             }
         };
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
index a53f4ba..d1e6ee6 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
@@ -28,6 +28,7 @@
 import android.content.ServiceConnection;
 import android.os.IBinder;
 import android.util.Log;
+import android.util.TypedValue;
 
 import androidx.annotation.NonNull;
 import androidx.core.util.Preconditions;
@@ -36,6 +37,7 @@
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Transformations;
 
+import java.text.DecimalFormat;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -48,6 +50,9 @@
 
     private static final int PROPERTIES_REFRESH_RATE_UI = 5;
 
+    private float mSpeedFactor;
+    private float mDistanceFactor;
+
     public enum NavigationActivityState {
         /** No activity has been selected to be displayed on the navigation fragment yet */
         NOT_SELECTED,
@@ -110,42 +115,43 @@
         }
     }
 
-
     private CarPropertyManager.CarPropertyEventListener mCarPropertyEventListener =
             new CarPropertyManager.CarPropertyEventListener() {
-        @Override
-        public void onChangeEvent(CarPropertyValue value) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "CarProperty change: property " + value.getPropertyId() + ", area"
-                        + value.getAreaId() + ", value: " + value.getValue());
-            }
-            for (Sensor<?> sensorId : Sensors.getInstance()
-                    .getSensorsForPropertyId(value.getPropertyId())) {
-                if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID
-                        || (sensorId.mAreaId & value.getAreaId()) != 0) {
-                    setSensorValue(sensorId, value);
+                @Override
+                public void onChangeEvent(CarPropertyValue value) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG,
+                                "CarProperty change: property " + value.getPropertyId() + ", area"
+                                        + value.getAreaId() + ", value: " + value.getValue());
+                    }
+                    for (Sensor<?> sensorId : Sensors.getInstance()
+                            .getSensorsForPropertyId(value.getPropertyId())) {
+                        if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID
+                                || (sensorId.mAreaId & value.getAreaId()) != 0) {
+                            setSensorValue(sensorId, value);
+                        }
+                    }
                 }
-            }
-        }
 
-        @Override
-        public void onErrorEvent(int propId, int zone) {
-            for (Sensor<?> sensorId : Sensors.getInstance().getSensorsForPropertyId(propId)) {
-                if (sensorId.mAreaId == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL
-                        || (sensorId.mAreaId & zone) != 0) {
-                    setSensorValue(sensorId, null);
+                @Override
+                public void onErrorEvent(int propId, int zone) {
+                    for (Sensor<?> sensorId : Sensors.getInstance().getSensorsForPropertyId(
+                            propId)) {
+                        if (sensorId.mAreaId == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL
+                                || (sensorId.mAreaId & zone) != 0) {
+                            setSensorValue(sensorId, null);
+                        }
+                    }
                 }
-            }
-        }
 
-        private <T> void setSensorValue(Sensor<T> id, CarPropertyValue<?> value) {
-            T newValue = value != null ? id.mAdapter.apply(value) : null;
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Sensor " + id.mName + " = " + newValue);
-            }
-            getSensorMutableLiveData(id).setValue(newValue);
-        }
-    };
+                private <T> void setSensorValue(Sensor<T> id, CarPropertyValue<?> value) {
+                    T newValue = value != null ? id.mAdapter.apply(value) : null;
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Sensor " + id.mName + " = " + newValue);
+                    }
+                    getSensorMutableLiveData(id).setValue(newValue);
+                }
+            };
 
     /**
      * New {@link ClusterViewModel} instance
@@ -154,6 +160,13 @@
         super(application);
         mCar = Car.createCar(application, mCarServiceConnection);
         mCar.connect();
+
+        TypedValue tv = new TypedValue();
+        getApplication().getResources().getValue(R.dimen.speed_factor, tv, true);
+        mSpeedFactor = tv.getFloat();
+
+        getApplication().getResources().getValue(R.dimen.distance_factor, tv, true);
+        mDistanceFactor = tv.getFloat();
     }
 
     @Override
@@ -187,7 +200,7 @@
      * own data type. The list of all supported sensors can be found at {@link Sensors}
      *
      * @param sensor sensor to observe
-     * @param <T> data type of such sensor
+     * @param <T>    data type of such sensor
      */
     @SuppressWarnings("unchecked")
     @NonNull
@@ -199,8 +212,8 @@
      * Returns the current value of the sensor, directly from the VHAL.
      *
      * @param sensor sensor to read
-     * @param <V> VHAL data type
-     * @param <T> data type of such sensor
+     * @param <V>    VHAL data type
+     * @param <T>    data type of such sensor
      */
     @Nullable
     public <T> T getSensorValue(@NonNull Sensor<T> sensor) {
@@ -210,21 +223,48 @@
     }
 
     /**
-     * Returns a {@link LiveData} that tracks the fuel level in a range from 0.0 to 1.0.
+     * Returns a {@link LiveData} that tracks the fuel level in a range from 0 to 100.
      */
-    public LiveData<Float> getFuelLevel() {
+    public LiveData<Integer> getFuelLevel() {
         return Transformations.map(getSensor(Sensors.SENSOR_FUEL), (fuelValue) -> {
             Float fuelCapacityValue = getSensorValue(Sensors.SENSOR_FUEL_CAPACITY);
             if (fuelValue == null || fuelCapacityValue == null || fuelCapacityValue == 0) {
                 return null;
             }
             if (fuelValue < 0.0f) {
-                return 0.0f;
+                return 0;
             }
             if (fuelValue > fuelCapacityValue) {
-                return 1.0f;
+                return 100;
             }
-            return fuelValue / fuelCapacityValue;
+            return Math.round(fuelValue / (fuelCapacityValue * 100f));
+        });
+    }
+
+    /**
+     * Returns a {@link LiveData} that tracks the RPM x 1000
+     */
+    public LiveData<String> getRPM() {
+        return Transformations.map(getSensor(Sensors.SENSOR_RPM), (rpmValue) -> {
+            return new DecimalFormat("#0.0").format(rpmValue / 1000f);
+        });
+    }
+
+    /**
+     * Returns a {@link LiveData} that tracks the speed in either mi/h or km/h depending on locale.
+     */
+    public LiveData<Integer> getSpeed() {
+        return Transformations.map(getSensor(Sensors.SENSOR_SPEED), (speedValue) -> {
+            return Math.round(speedValue * mSpeedFactor);
+        });
+    }
+
+    /**
+     * Returns a {@link LiveData} that tracks the range the vehicle has until it runs out of gas.
+     */
+    public LiveData<Integer> getRange() {
+        return Transformations.map(getSensor(Sensors.SENSOR_FUEL_RANGE), (rangeValue) -> {
+            return Math.round(rangeValue / mDistanceFactor);
         });
     }
 
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
index e0d0d12..ae86850 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
@@ -18,19 +18,34 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
+import android.os.Handler;
 import android.text.SpannableStringBuilder;
 import android.text.style.ImageSpan;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.widget.TextView;
 
+import androidx.car.cluster.navigation.ImageReference;
 import androidx.car.cluster.navigation.RichText;
 import androidx.car.cluster.navigation.RichTextElement;
 
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
 /**
  * View component that displays the Cue information on the instrument cluster display
  */
 public class CueView extends TextView {
+    private static final String TAG = "Cluster.CueView";
+
     private String mImageSpanText;
+    private CompletableFuture<?> mFuture;
+    private Handler mHandler = new Handler();
+    private RichText mContent;
 
     public CueView(Context context) {
         super(context);
@@ -45,20 +60,45 @@
         mImageSpanText = context.getString(R.string.span_image);
     }
 
-    public void setRichText(RichText richText) {
+    public void setRichText(RichText richText, ImageResolver imageResolver) {
         if (richText == null) {
             setText(null);
             return;
         }
 
+        if (mFuture != null && !Objects.equals(richText, mContent)) {
+            mFuture.cancel(true);
+        }
+
+        List<ImageReference> imageReferences = richText.getElements().stream()
+                .filter(element -> element.getImage() != null)
+                .map(element -> element.getImage())
+                .collect(Collectors.toList());
+        mFuture = imageResolver
+                .getBitmaps(imageReferences, 0, getLineHeight())
+                .thenAccept(bitmaps -> {
+                    mHandler.post(() -> update(richText, bitmaps));
+                    mFuture = null;
+                })
+                .exceptionally(ex -> {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Unable to fetch images for cue: " + richText);
+                    }
+                    mHandler.post(() -> update(richText, Collections.emptyMap()));
+                    return null;
+                });
+        mContent = richText;
+    }
+
+    private void update(RichText richText, Map<ImageReference, Bitmap> bitmaps) {
         SpannableStringBuilder builder = new SpannableStringBuilder();
+
         for (RichTextElement element : richText.getElements()) {
             if (builder.length() > 0) {
                 builder.append(" ");
             }
             if (element.getImage() != null) {
-                Bitmap bitmap = ImageResolver.getInstance().getBitmapConstrained(getContext(),
-                        element.getImage(), 0, getLineHeight());
+                Bitmap bitmap = bitmaps.get(element.getImage());
                 if (bitmap != null) {
                     String imageText = element.getText().isEmpty() ? mImageSpanText :
                             element.getText();
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
index f306143..5e03b9b 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
@@ -15,95 +15,126 @@
  */
 package android.car.cluster.sample;
 
-import android.content.ContentResolver;
-import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.Point;
 import android.net.Uri;
-import android.os.ParcelFileDescriptor;
 import android.util.Log;
+import android.util.LruCache;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.cluster.navigation.ImageReference;
 
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 
 /**
  * Class for retrieving bitmap images from a ContentProvider
  */
 public class ImageResolver {
     private static final String TAG = "Cluster.ImageResolver";
+    private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
 
-    private static ImageResolver sImageResolver = new ImageResolver();
+    private final BitmapFetcher mFetcher;
+    private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
+            IMAGE_CACHE_SIZE_BYTES) {
+        @Override
+        protected int sizeOf(String key, Bitmap value) {
+            return value.getByteCount();
+        }
+    };
 
-    private ImageResolver() {}
-
-    public static ImageResolver getInstance() {
-        return sImageResolver;
+    public interface BitmapFetcher {
+        Bitmap getBitmap(Uri uri);
     }
 
     /**
-     * Returns a bitmap from an URI string from a content provider
-     *
-     * @param context View context
+     * Creates a resolver that delegate the image retrieval to the given fetcher.
      */
-    @Nullable
-    public Bitmap getBitmap(Context context, Uri uri) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Requesting: " + uri);
-        }
-        try {
-            ContentResolver contentResolver = context.getContentResolver();
-            ParcelFileDescriptor fileDesc = contentResolver.openFileDescriptor(uri, "r");
-            if (fileDesc != null) {
-                Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
-                fileDesc.close();
-                return bitmap;
-            } else {
-                Log.e(TAG, "Null pointer: Failed to create pipe for uri string: " + uri);
-            }
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "File not found for uri string: " + uri, e);
-        } catch (IOException e) {
-            Log.e(TAG, "File descriptor could not close: ", e);
-        }
-
-        return null;
+    public ImageResolver(BitmapFetcher fetcher) {
+        mFetcher = fetcher;
     }
 
     /**
-     * Returns a bitmap from a Car Instrument Cluster {@link ImageReference} that would fit inside
-     * the provided size. Either width, height or both should be greater than 0.
+     * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}.
+     * This image would fit inside the provided size. Either width, height or both should be greater
+     * than 0.
      *
-     * @param context View context
      * @param width required width, or 0 if width is flexible based on height.
      * @param height required height, or 0 if height is flexible based on width.
      */
-    @Nullable
-    public Bitmap getBitmapConstrained(Context context, ImageReference img, int width,
-            int height) {
+    @NonNull
+    public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height) {
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, String.format("Requesting image %s (width: %d, height: %d)",
                     img.getRawContentUri(), width, height));
         }
 
-        // Adjust the size to fit in the requested box.
-        Point adjusted = getAdjustedSize(img.getOriginalWidth(), img.getOriginalHeight(), width,
-                height);
-        if (adjusted == null) {
-            Log.e(TAG, "The provided image has no original size: " + img.getRawContentUri());
-            return null;
-        }
-        Bitmap bitmap = getBitmap(context, img.getContentUri(adjusted.x, adjusted.y));
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)",
-                    img.getRawContentUri(), width, height));
-        }
-        return bitmap != null ? Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true)
-                : null;
+        return CompletableFuture.supplyAsync(() -> {
+            // Adjust the size to fit in the requested box.
+            Point adjusted = getAdjustedSize(img.getOriginalWidth(), img.getOriginalHeight(), width,
+                    height);
+            if (adjusted == null) {
+                Log.e(TAG, "The provided image has no original size: " + img.getRawContentUri());
+                return null;
+            }
+            Uri uri = img.getContentUri(adjusted.x, adjusted.y);
+            Bitmap bitmap = mCache.get(uri.toString());
+            if (bitmap == null) {
+                bitmap = mFetcher.getBitmap(uri);
+                if (bitmap == null) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Unable to fetch image: " + uri);
+                    }
+                    return null;
+                }
+                if (bitmap.getWidth() != adjusted.x || bitmap.getHeight() != adjusted.y) {
+                    bitmap = Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true);
+                }
+                mCache.put(uri.toString(), bitmap);
+            }
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)",
+                        img.getRawContentUri(), width, height));
+            }
+            return bitmap != null ? Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true)
+                    : null;
+        });
+    }
+
+    /**
+     * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The
+     * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to
+     * its bitmap. If any image fails to be fetched, the whole future completes exceptionally.
+     *
+     * @param width required width, or 0 if width is flexible based on height.
+     * @param height required height, or 0 if height is flexible based on width.
+     */
+    @NonNull
+    public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps(
+            @NonNull List<ImageReference> imgs, int width, int height) {
+        CompletableFuture<Map<ImageReference, Bitmap>> future = new CompletableFuture<>();
+
+        Map<ImageReference, CompletableFuture<Bitmap>> bitmapFutures = imgs.stream().collect(
+                Collectors.toMap(
+                        img -> img,
+                        img -> getBitmap(img, width, height)));
+
+        CompletableFuture.allOf(bitmapFutures.values().toArray(new CompletableFuture[0]))
+                .thenAccept(v -> {
+                    Map<ImageReference, Bitmap> bitmaps = bitmapFutures.entrySet().stream()
+                            .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry
+                                    .getValue().join()));
+                    future.complete(bitmaps);
+                })
+                .exceptionally(ex -> {
+                    future.completeExceptionally(ex);
+                    return null;
+                });
+
+        return future;
     }
 
     /**
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 3181b1e..9f4f931 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -16,17 +16,6 @@
 package android.car.cluster.sample;
 
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.LOCAL_BINDING_ACTION;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
-        .MSG_ON_NAVIGATION_STATE_CHANGED;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_REGISTER_CLIENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
-        .MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
 import static android.content.Intent.ACTION_USER_SWITCHED;
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 
@@ -48,9 +37,6 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.SparseArray;
@@ -69,7 +55,6 @@
 import androidx.fragment.app.FragmentPagerAdapter;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.ViewModelProviders;
-import androidx.versionedparcelable.ParcelUtils;
 import androidx.viewpager.widget.ViewPager;
 
 import java.lang.ref.WeakReference;
@@ -95,7 +80,8 @@
  * This is necessary because the navigation app runs under a normal user, and different users will
  * see different instances of the same application, with their own personalized data.
  */
-public class MainClusterActivity extends FragmentActivity {
+public class MainClusterActivity extends FragmentActivity implements
+        ClusterRenderingServiceImpl.ServiceClient {
     private static final String TAG = "Cluster.MainActivity";
 
     private static final NavigationState NULL_NAV_STATE = new NavigationState.Builder().build();
@@ -110,8 +96,7 @@
 
     private Map<Sensors.Gear, View> mGearsToIcon = new HashMap<>();
     private InputMethodManager mInputMethodManager;
-    private Messenger mService;
-    private Messenger mServiceCallbacks = new Messenger(new MessageHandler(this));
+    private ClusterRenderingServiceImpl mService;
     private VirtualDisplay mPendingVirtualDisplay = null;
 
     private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
@@ -142,7 +127,7 @@
                 @Override
                 public void onFocusChange(View v, boolean hasFocus) {
                     if (hasFocus) {
-                        mPager.setCurrentItem(mButtonToFacet.get(v).order);
+                        mPager.setCurrentItem(mButtonToFacet.get(v).mOrder);
                     }
                 }
             };
@@ -151,8 +136,9 @@
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
-            mService = new Messenger(service);
-            sendServiceMessage(MSG_REGISTER_CLIENT, null, mServiceCallbacks);
+            mService = ((ClusterRenderingServiceImpl.LocalBinder) service).getService();
+            mService.registerClient(MainClusterActivity.this);
+            mNavStateController.setImageResolver(mService.getImageResolver());
             if (mPendingVirtualDisplay != null) {
                 // If haven't reported the virtual display yet, do so on service connect.
                 reportNavDisplay(mPendingVirtualDisplay);
@@ -164,44 +150,11 @@
         public void onServiceDisconnected(ComponentName name) {
             Log.i(TAG, "onServiceDisconnected, name: " + name);
             mService = null;
+            mNavStateController.setImageResolver(null);
             onNavigationStateChange(NULL_NAV_STATE);
         }
     };
 
-    private static class MessageHandler extends Handler {
-        private final WeakReference<MainClusterActivity> mActivity;
-
-        MessageHandler(MainClusterActivity activity) {
-            mActivity = new WeakReference<>(activity);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            Bundle data = msg.getData();
-            switch (msg.what) {
-                case MSG_ON_KEY_EVENT:
-                    KeyEvent event = data.getParcelable(MSG_KEY_KEY_EVENT);
-                    if (event != null) {
-                        mActivity.get().onKeyEvent(event);
-                    }
-                    break;
-                case MSG_ON_NAVIGATION_STATE_CHANGED:
-                    if (data == null) {
-                        mActivity.get().onNavigationStateChange(null);
-                    } else {
-                        data.setClassLoader(ParcelUtils.class.getClassLoader());
-                        NavigationState navState = NavigationState
-                                .fromParcelable(data.getParcelable(
-                                        ClusterRenderingServiceImpl.NAV_STATE_BUNDLE_KEY));
-                        mActivity.get().onNavigationStateChange(navState);
-                    }
-                    break;
-                default:
-                    super.handleMessage(msg);
-            }
-        }
-    }
-
     private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
         if (displayId != mNavigationDisplayId) {
             return;
@@ -259,7 +212,7 @@
 
         mPager = findViewById(R.id.pager);
         mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
-        mOrderToFacet.get(0).button.requestFocus();
+        mOrderToFacet.get(0).mButton.requestFocus();
         mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
 
         mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
@@ -274,12 +227,9 @@
         mClusterViewModel.getSensor(Sensors.SENSOR_GEAR).observe(this, this::updateSelectedGear);
 
         registerSensor(findViewById(R.id.info_fuel), mClusterViewModel.getFuelLevel());
-        registerSensor(findViewById(R.id.info_speed),
-                mClusterViewModel.getSensor(Sensors.SENSOR_SPEED));
-        registerSensor(findViewById(R.id.info_range),
-                mClusterViewModel.getSensor(Sensors.SENSOR_FUEL_RANGE));
-        registerSensor(findViewById(R.id.info_rpm),
-                mClusterViewModel.getSensor(Sensors.SENSOR_RPM));
+        registerSensor(findViewById(R.id.info_speed), mClusterViewModel.getSpeed());
+        registerSensor(findViewById(R.id.info_range), mClusterViewModel.getRange());
+        registerSensor(findViewById(R.id.info_rpm), mClusterViewModel.getRPM());
 
         mActivityMonitor.start();
 
@@ -300,13 +250,14 @@
         mUserReceiver.unregister(this);
         mActivityMonitor.stop();
         if (mService != null) {
-            sendServiceMessage(MSG_UNREGISTER_CLIENT, null, mServiceCallbacks);
+            mService.unregisterClient(this);
             mService = null;
         }
         unbindService(mClusterRenderingServiceConnection);
     }
 
-    private void onKeyEvent(KeyEvent event) {
+    @Override
+    public void onKeyEvent(KeyEvent event) {
         Log.i(TAG, "onKeyEvent, event: " + event);
 
         // This is a hack. We use SOURCE_CLASS_POINTER here because this type of input is associated
@@ -316,7 +267,8 @@
         mInputMethodManager.dispatchKeyEventFromInputMethod(getCurrentFocus(), event);
     }
 
-    private void onNavigationStateChange(NavigationState state) {
+    @Override
+    public void onNavigationStateChange(NavigationState state) {
         Log.d(TAG, "onNavigationStateChange: " + state);
         if (mNavStateController != null) {
             mNavStateController.update(state);
@@ -338,32 +290,9 @@
     }
 
     private void reportNavDisplay(VirtualDisplay virtualDisplay) {
-        Bundle data = new Bundle();
-        data.putString(MSG_KEY_CATEGORY, Car.CAR_CATEGORY_NAVIGATION);
-        data.putInt(MSG_KEY_ACTIVITY_DISPLAY_ID, virtualDisplay.mDisplayId);
-        data.putBundle(MSG_KEY_ACTIVITY_STATE, ClusterActivityState
+        mService.setActivityLaunchOptions(virtualDisplay.mDisplayId, ClusterActivityState
                 .create(virtualDisplay.mDisplayId != Display.INVALID_DISPLAY,
-                        virtualDisplay.mUnobscuredBounds)
-                .toBundle());
-        sendServiceMessage(MSG_SET_ACTIVITY_LAUNCH_OPTIONS, data, null);
-    }
-
-    /**
-     * Sends a message to the {@link ClusterRenderingServiceImpl}, which runs on a different
-     * process.
-     * @param what action to perform
-     * @param data action data
-     * @param replyTo {@link Messenger} where to reply back
-     */
-    private void sendServiceMessage(int what, Bundle data, Messenger replyTo) {
-        try {
-            Message message = Message.obtain(null, what);
-            message.setData(data);
-            message.replyTo = replyTo;
-            mService.send(message);
-        } catch (RemoteException ex) {
-            Log.e(TAG, "Unable to deliver message " + what + ". Service died");
-        }
+                        virtualDisplay.mUnobscuredBounds));
     }
 
     public class ClusterPageAdapter extends FragmentPagerAdapter {
@@ -383,21 +312,21 @@
     }
 
     private <T> void registerFacet(Facet<T> facet) {
-        mOrderToFacet.append(facet.order, facet);
-        mButtonToFacet.put(facet.button, facet);
+        mOrderToFacet.append(facet.mOrder, facet);
+        mButtonToFacet.put(facet.mButton, facet);
 
-        facet.button.setOnFocusChangeListener(mFacetButtonFocusListener);
+        facet.mButton.setOnFocusChangeListener(mFacetButtonFocusListener);
     }
 
     private static class Facet<T> {
-        Button button;
-        Class<T> clazz;
-        int order;
+        Button mButton;
+        Class<T> mClazz;
+        int mOrder;
 
         Facet(Button button, int order, Class<T> clazz) {
-            this.button = button;
-            this.order = order;
-            this.clazz = clazz;
+            this.mButton = button;
+            this.mOrder = order;
+            this.mClazz = clazz;
         }
 
         private Fragment mFragment;
@@ -405,8 +334,9 @@
         Fragment getOrCreateFragment() {
             if (mFragment == null) {
                 try {
-                    mFragment = (Fragment) clazz.getConstructors()[0].newInstance();
-                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+                    mFragment = (Fragment) mClazz.getConstructors()[0].newInstance();
+                } catch (InstantiationException | IllegalAccessException
+                        | InvocationTargetException e) {
                     throw new RuntimeException(e);
                 }
             }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index 0d07962..3ba31d7 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -46,6 +46,7 @@
     private TextView mEta;
     private CueView mCue;
     private Context mContext;
+    private ImageResolver mImageResolver;
 
     /**
      * Creates a controller to coordinate updates to the views displaying navigation state
@@ -63,6 +64,10 @@
         mContext = container.getContext();
     }
 
+    public void setImageResolver(@Nullable ImageResolver imageResolver) {
+        mImageResolver = imageResolver;
+    }
+
     /**
      * Updates views to reflect the provided navigation state
      */
@@ -80,7 +85,7 @@
         mEta.setTextColor(getTrafficColor(traffic));
         mManeuver.setImageDrawable(getManeuverIcon(step != null ? step.getManeuver() : null));
         mDistance.setText(formatDistance(step != null ? step.getDistance() : null));
-        mCue.setRichText(step != null ? step.getCue() : null);
+        mCue.setRichText(step != null ? step.getCue() : null, mImageResolver);
 
         if (step != null && step.getLanes().size() > 0) {
             mLane.setLanes(step.getLanes());
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/sensors/Sensors.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/sensors/Sensors.java
index 6e49931..90d6350 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/sensors/Sensors.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/sensors/Sensors.java
@@ -60,12 +60,12 @@
             "RPM", VehiclePropertyIds.ENGINE_RPM, GLOBAL_AREA_ID,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
-    /** Fuel range in kilometers */
+    /** Fuel range in meters */
     public static final Sensor<Float> SENSOR_FUEL_RANGE = registerSensor(
             "Fuel Range", VehiclePropertyIds.RANGE_REMAINING, GLOBAL_AREA_ID,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
-    /** Speed in kph */
+    /** Speed in meters per second */
     public static final Sensor<Float> SENSOR_SPEED = registerSensor(
             "Speed", VehiclePropertyIds.PERF_VEHICLE_SPEED, GLOBAL_AREA_ID,
             VehiclePropertyType.FLOAT,
diff --git a/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java b/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
index 746c0df..0d1cece 100644
--- a/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
+++ b/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
@@ -32,7 +32,7 @@
 
     @Before
     public void setup() {
-        mImageResolver = ImageResolver.getInstance();
+        mImageResolver = new ImageResolver((uri) -> null);
     }
 
     @Test
diff --git a/tests/UxRestrictionsSample/Android.mk b/tests/UxRestrictionsSample/Android.mk
index 50419f4..3e006b4 100644
--- a/tests/UxRestrictionsSample/Android.mk
+++ b/tests/UxRestrictionsSample/Android.mk
@@ -42,7 +42,6 @@
 LOCAL_STATIC_JAVA_LIBRARIES += vehicle-hal-support-lib
 
 LOCAL_STATIC_ANDROID_LIBRARIES += \
-    androidx.car_car \
     androidx.legacy_legacy-support-v4 \
     androidx.appcompat_appcompat
 
diff --git a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml b/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
deleted file mode 100644
index a778975..0000000
--- a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 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">
-  <Button
-      android:id="@+id/home_button"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:text="@string/return_home"
-      android:textAllCaps="false"
-      android:textAppearance="?android:textAppearanceLarge"/>
-  <LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content">
-    <androidx.car.widget.PagedListView
-        android:id="@+id/paged_list_view"
-        android:layout_height="match_parent"
-        android:layout_width="wrap_content"
-        android:layout_weight="4"/>
-  </LinearLayout>
-</LinearLayout>
diff --git a/tests/UxRestrictionsSample/res/layout/main_activity.xml b/tests/UxRestrictionsSample/res/layout/main_activity.xml
index 0b097fb..1d21b50 100644
--- a/tests/UxRestrictionsSample/res/layout/main_activity.xml
+++ b/tests/UxRestrictionsSample/res/layout/main_activity.xml
@@ -125,27 +125,6 @@
         android:layout_marginBottom="10dp"
         android:background="@android:color/darker_gray"/>
 
-    <TextView
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="@string/sample_header"
-        android:padding="@dimen/section_padding"
-        android:textSize="@dimen/header_text_size"
-        android:textAppearance="?android:textAppearanceLarge"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-      <Button
-          android:id="@+id/launch_message"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:padding="@dimen/section_padding"
-          android:text="@string/sample_msg_activity"
-          android:textAllCaps="false"
-          android:textSize="@dimen/info_text_size"/>
-    </LinearLayout>
-
     <View
         android:layout_width="match_parent"
         android:layout_height="1dp"
diff --git a/tests/UxRestrictionsSample/res/values/strings.xml b/tests/UxRestrictionsSample/res/values/strings.xml
index b36d7f9..aae0e0d 100644
--- a/tests/UxRestrictionsSample/res/values/strings.xml
+++ b/tests/UxRestrictionsSample/res/values/strings.xml
@@ -25,9 +25,6 @@
     <string name="disable_uxr" translatable="false">Disable Ux Restriction Engine</string>
     <string name="show_staged_config" translatable="false">Show Staged Config</string>
     <string name="show_prod_config" translatable="false">Show Production Config</string>
-    <string name="sample_header" translatable="false"><u>Sample Activities</u></string>
-    <string name="sample_msg_activity" translatable="false">Sample Message Activity</string>
-    <string name="return_home" translatable="false"><u>Return Home</u></string>
     <string name="save_uxr_config_header" translatable="false"><u>Save UX Restrictions For Next Boot</u></string>
     <string name="save_uxr_config" translatable="false">Save UX Restrictions</string>
     <string name="set_uxr_config_dialog_title" translatable="false">Select restrictions for IDLING/MOVING</string>
diff --git a/tests/UxRestrictionsSample/res/values/styles.xml b/tests/UxRestrictionsSample/res/values/styles.xml
index 05d0d03..b752323 100644
--- a/tests/UxRestrictionsSample/res/values/styles.xml
+++ b/tests/UxRestrictionsSample/res/values/styles.xml
@@ -15,7 +15,7 @@
 -->
 <resources>
 
-    <style name="AppTheme" parent="@style/Theme.Car.NoActionBar">
+    <style name="AppTheme" parent="@style/android:Theme.DeviceDefault.NoActionBar">
         <item name="android:windowBackground">@android:color/black</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
index 0fa9259..60268ea 100644
--- a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
+++ b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
@@ -29,13 +29,11 @@
 import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.CarUxRestrictionsManager;
 import android.content.ComponentName;
-import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.JsonWriter;
 import android.util.Log;
-import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -188,9 +186,6 @@
         mShowProdConfig.setOnClickListener(v -> showProdUxRestrictionsConfig());
         mToggleButton.setOnClickListener(v -> updateToggleUxREnable());
 
-        mSampleMsgButton = findViewById(R.id.launch_message);
-        mSampleMsgButton.setOnClickListener(this::launchSampleMsgActivity);
-
         // Connect to car service
         mCar = Car.createCar(this, mCarConnectionListener);
         mCar.connect();
@@ -273,11 +268,6 @@
         }
     }
 
-    private void launchSampleMsgActivity(View view) {
-        Intent msgIntent = new Intent(this, SampleMessageActivity.class);
-        startActivity(msgIntent);
-    }
-
     @Override
     protected void onDestroy() {
         super.onDestroy();
diff --git a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/SampleMessageActivity.java b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/SampleMessageActivity.java
deleted file mode 100644
index 1c9bc95..0000000
--- a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/SampleMessageActivity.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2018 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.uxr.sample;
-
-import android.annotation.DrawableRes;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.Button;
-
-import androidx.car.widget.ListItem;
-import androidx.car.widget.ListItemAdapter;
-import androidx.car.widget.ListItemProvider;
-import androidx.car.widget.PagedListView;
-import androidx.car.widget.TextListItem;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A Sample Messaging Activity that illustrates how to truncate the string length on receiving the
- * corresponding UX restriction.
- */
-public class SampleMessageActivity extends Activity {
-    private Button mHomeButton;
-    private PagedListView mPagedListView;
-    private ListItemAdapter mAdapter;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_sample_message);
-        mHomeButton = findViewById(R.id.home_button);
-        mHomeButton.setOnClickListener(this::returnHome);
-
-        mPagedListView = findViewById(R.id.paged_list_view);
-        setUpPagedListView();
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        if (mAdapter != null) {
-            mAdapter.start();
-        }
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        if (mAdapter != null) {
-            mAdapter.stop();
-        }
-    }
-
-    private void returnHome(View view) {
-        Intent homeIntent = new Intent(this, MainActivity.class);
-        startActivity(homeIntent);
-    }
-
-    private void setUpPagedListView() {
-        mAdapter = new ListItemAdapter(this, populateData());
-        mPagedListView.setAdapter(mAdapter);
-    }
-
-    private ListItemProvider populateData() {
-        List<ListItem> items = new ArrayList<>();
-        items.add(createMessage(android.R.drawable.ic_menu_myplaces, "alice",
-                "i have a really important message but it may hinder your ability to drive. "));
-
-        items.add(createMessage(android.R.drawable.ic_menu_myplaces, "bob",
-                "hey this is a really long message that i have always wanted to say. but before "
-                        + "saying it i feel it's only appropriate if i lay some groundwork for it. "
-                        + ""));
-        items.add(createMessage(android.R.drawable.ic_menu_myplaces, "mom",
-                "i think you are the best. i think you are the best. i think you are the best. "
-                        + "i think you are the best. i think you are the best. i think you are the "
-                        + "best. "
-                        + "i think you are the best. i think you are the best. i think you are the "
-                        + "best. "
-                        + "i think you are the best. i think you are the best. i think you are the "
-                        + "best. "
-                        + "i think you are the best. i think you are the best. i think you are the "
-                        + "best. "
-                        + "i think you are the best. i think you are the best. "));
-        items.add(createMessage(android.R.drawable.ic_menu_myplaces, "john", "hello world"));
-        items.add(createMessage(android.R.drawable.ic_menu_myplaces, "jeremy",
-                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "
-                        + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, "
-                        + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo "
-                        + "consequat. Duis aute irure dolor in reprehenderit in voluptate velit "
-                        + "esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat "
-                        + "cupidatat non proident, sunt in culpa qui officia deserunt mollit "
-                        + "anim id est laborum."));
-        return new ListItemProvider.ListProvider(items);
-    }
-
-    private TextListItem createMessage(@DrawableRes int profile, String contact, String message) {
-        TextListItem item = new TextListItem(this);
-        item.setPrimaryActionIcon(profile, TextListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
-        item.setTitle(contact);
-        item.setBody(message);
-        item.setSupplementalIcon(android.R.drawable.stat_notify_chat, false);
-        return item;
-    }
-
-}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
index abfeb11..db04c24 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
@@ -85,7 +85,7 @@
         try {
             mManager.registerProjectionListener(null, CarProjectionManager.PROJECTION_VOICE_SEARCH);
             fail();
-        } catch (IllegalArgumentException e) {
+        } catch (NullPointerException e) {
             // expected.
         }
     }
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
index 5ee92d7..3c545d6 100644
--- a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -96,6 +96,7 @@
     @After
     public void tearDown() throws Exception {
         mService = null;
+        CarLocalServices.removeAllServices();
     }
 
     @Test
diff --git a/tests/carservice_test/src/com/android/car/VmsHalServiceSubscriptionEventTest.java b/tests/carservice_test/src/com/android/car/VmsHalServiceSubscriptionEventTest.java
index ad586ae..1eb0a95 100644
--- a/tests/carservice_test/src/com/android/car/VmsHalServiceSubscriptionEventTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsHalServiceSubscriptionEventTest.java
@@ -28,7 +28,6 @@
 import android.hardware.automotive.vehicle.V2_0.VmsAvailabilityStateIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsBaseMessageIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
-import android.hardware.automotive.vehicle.V2_0.VmsMessageWithLayerIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsSubscriptionsStateIntegerValuesIndex;
 
 import androidx.test.filters.MediumTest;
@@ -106,13 +105,14 @@
         assertTrue(mHalHandlerSemaphore.tryAcquire(2L, TimeUnit.SECONDS));
         // Validate response.
         ArrayList<Integer> v = mHalHandler.getValues();
-        int messageType = v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
-        int sequenceNumber = v.get(VmsAvailabilityStateIntegerValuesIndex.SEQUENCE_NUMBER);
-        assertEquals(VmsMessageType.AVAILABILITY_CHANGE, messageType);
-        assertEquals(0, sequenceNumber);
+        assertEquals(VmsMessageType.AVAILABILITY_CHANGE,
+                (int) v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE));
+        assertEquals(0, (int) v.get(VmsAvailabilityStateIntegerValuesIndex.SEQUENCE_NUMBER));
 
+        int sequenceNumber = 0;
         for (VmsLayer layer : layers) {
-            subscribeViaHal(layer);
+            sequenceNumber++;
+            subscribeViaHal(sequenceNumber, layer);
         }
         // Send subscription request.
         mHal.injectEvent(createHalSubscriptionRequest());
@@ -122,14 +122,15 @@
 
         // Validate response.
         v = mHalHandler.getValues();
-        messageType = v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
-        assertEquals(VmsMessageType.SUBSCRIPTIONS_RESPONSE, messageType);
-        sequenceNumber = v.get(VmsSubscriptionsStateIntegerValuesIndex.SEQUENCE_NUMBER);
-        int numberLayers = v.get(VmsSubscriptionsStateIntegerValuesIndex.NUMBER_OF_LAYERS);
-        assertEquals(layers.size(), numberLayers);
+        assertEquals(VmsMessageType.SUBSCRIPTIONS_RESPONSE,
+                (int) v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE));
+        assertEquals(sequenceNumber,
+                (int) v.get(VmsSubscriptionsStateIntegerValuesIndex.SEQUENCE_NUMBER));
+        assertEquals(layers.size(),
+                (int) v.get(VmsSubscriptionsStateIntegerValuesIndex.NUMBER_OF_LAYERS));
         List<VmsLayer> receivedLayers = new ArrayList<>();
         int start = VmsSubscriptionsStateIntegerValuesIndex.SUBSCRIPTIONS_START;
-        int end = VmsSubscriptionsStateIntegerValuesIndex.SUBSCRIPTIONS_START + 3 * numberLayers;
+        int end = VmsSubscriptionsStateIntegerValuesIndex.SUBSCRIPTIONS_START + 3 * layers.size();
         while (start < end) {
             int type = v.get(start++);
             int subtype = v.get(start++);
@@ -140,23 +141,22 @@
     }
 
     /**
-     * Subscribes to a layer, waits for the event to propagate back to the HAL layer and validates
-     * the propagated message.
+     * Subscribes to a layer, waits for the state change to propagate back to the HAL layer and
+     * validates the propagated message.
      */
-    private void subscribeViaHal(VmsLayer layer) throws Exception {
+    private void subscribeViaHal(int sequenceNumber, VmsLayer layer) throws Exception {
         // Send subscribe request.
         mHal.injectEvent(createHalSubscribeRequest(layer));
         // Wait for response.
         assertTrue(mHalHandlerSemaphore.tryAcquire(2L, TimeUnit.SECONDS));
         // Validate response.
         ArrayList<Integer> v = mHalHandler.getValues();
-        int messsageType = v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
-        assertEquals(VmsMessageType.SUBSCRIBE, messsageType);
-        int layerId = v.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_TYPE);
-        int layerVersion = v.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_VERSION);
-        int fused = v.get(VmsMessageWithLayerIntegerValuesIndex.LAYER_SUBTYPE);
-        assertEquals(layer.getType(), layerId);
-        assertEquals(layer.getVersion(), layerVersion);
+        assertEquals(VmsMessageType.SUBSCRIPTIONS_CHANGE,
+                (int) v.get(VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE));
+        assertEquals(sequenceNumber,
+                (int) v.get(VmsSubscriptionsStateIntegerValuesIndex.SEQUENCE_NUMBER));
+        assertEquals(sequenceNumber,
+                (int) v.get(VmsSubscriptionsStateIntegerValuesIndex.NUMBER_OF_LAYERS));
     }
 
     private VehiclePropValue createHalSubscribeRequest(VmsLayer layer) {
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
index 557816a..e1d9c90 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -16,8 +16,15 @@
 
 package com.android.car;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateShutdownParam;
 import android.os.RemoteException;
@@ -52,6 +59,7 @@
     private final MockWakeLockInterface mWakeLockInterface = new MockWakeLockInterface();
     private final MockIOInterface mIOInterface = new MockIOInterface();
     private final PowerSignalListener mPowerSignalListener = new PowerSignalListener();
+    private CarUserManagerHelper mCarUserManagerHelper;
 
     private MockedPowerHalService mPowerHal;
     private SystemInterface mSystemInterface;
@@ -68,6 +76,10 @@
             .withSystemStateInterface(mSystemStateInterface)
             .withWakeLockInterface(mWakeLockInterface)
             .withIOInterface(mIOInterface).build();
+        mCarUserManagerHelper = mock(CarUserManagerHelper.class);
+        doReturn(true).when(mCarUserManagerHelper).switchToUserId(anyInt());
+        doReturn(10).when(mCarUserManagerHelper).getInitialUser();
+        doReturn(10).when(mCarUserManagerHelper).getCurrentForegroundUserId();
     }
 
     @Override
@@ -83,7 +95,8 @@
      * Helper method to create mService and initialize a test case
      */
     private void initTest(int wakeupTime) throws Exception {
-        mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface);
+        mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface,
+                mCarUserManagerHelper);
         mService.init();
         CarPowerManagementService.setShutdownPrepareTimeout(0);
         mPowerHal.setSignalListener(mPowerSignalListener);
@@ -103,7 +116,6 @@
         mSystemInterface.setDisplayState(false);
         mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS);
         initTest(0);
-
         // Transition to ON state
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
 
@@ -160,6 +172,13 @@
     public void testSleepEntryAndWakeUpForProcessing() throws Exception {
         final int wakeupTime = 100;
         initTest(wakeupTime);
+
+        // set up for user switching after display on
+        final int currentUserId = 10;
+        final int newUserId = 11;
+        doReturn(newUserId).when(mCarUserManagerHelper).getInitialUser();
+        doReturn(currentUserId).when(mCarUserManagerHelper).getCurrentForegroundUserId();
+
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
         assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
@@ -177,8 +196,12 @@
         mService.scheduleNextWakeupTime(wakeupTime);
         // second processing after wakeup
         assertFalse(mDisplayInterface.getDisplayState());
+        // do not skip user switching part.
+        mService.clearIsBooting();
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
         assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+        // user switching should have been requested.
+        verify(mCarUserManagerHelper, times(1)).switchToUserId(newUserId);
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
                 VehicleApPowerStateShutdownParam.CAN_SLEEP));
         assertStateReceivedForShutdownOrSleepWithPostpone(
diff --git a/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
new file mode 100644
index 0000000..476c152
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 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 static android.car.projection.ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_USB;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_WIFI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.MobileDevice;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.net.wifi.WifiScanner;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class CarProjectionServiceTest {
+    private static final int MD_ID1 = 1;
+    private static final int MD_ID2 = 2;
+    private static final String MD_NAME1 = "Device1";
+    private static final String MD_NAME2 = "Device2";
+    private static final int DEFAULT_TIMEOUT_MS = 1000;
+    private static final String MD_EXTRA_KEY = "com.some.key.md";
+    private static final String MD_EXTRA_VALUE = "this is dummy value";
+    private static final String STATUS_EXTRA_KEY = "com.some.key.status";
+    private static final String STATUS_EXTRA_VALUE = "additional status value";
+
+    private final IBinder mToken = new Binder();
+
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+
+    private CarProjectionService mService;
+
+    @Spy
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Mock
+    private Resources mResources;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    @Mock
+    private CarInputService mCarInputService;
+    @Mock
+    private CarBluetoothService mCarBluetoothService;
+
+    @Before
+    public void setUp() {
+        mService = new CarProjectionService(mContext, mHandler, mCarInputService,
+                mCarBluetoothService);
+    }
+
+    @Test
+    public void updateProjectionStatus_defaultState() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(2);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+                assertThat(activeProjectionPackageName).isNull();
+                assertThat(details).isEmpty();
+
+                latch.countDown();
+            }
+        });
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void updateProjectionStatus_subscribeAfterUpdate() throws Exception {
+        final ProjectionStatus status = createProjectionStatus();
+        mService.updateProjectionStatus(status, mToken);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+                assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+                assertThat(details).hasSize(1);
+                assertThat(details.get(0)).isEqualTo(status);
+                ProjectionStatus status = details.get(0);
+                assertThat(status.getTransport()).isEqualTo(PROJECTION_TRANSPORT_WIFI);
+                assertThat(status.getExtras()).isNotNull();
+                assertThat(status.getExtras().getString(STATUS_EXTRA_KEY))
+                        .isEqualTo(STATUS_EXTRA_VALUE);
+                assertThat(status.getConnectedMobileDevices()).hasSize(2);
+                MobileDevice md1 = status.getConnectedMobileDevices().get(0);
+                assertThat(md1.getId()).isEqualTo(MD_ID1);
+                assertThat(md1.getName()).isEqualTo(MD_NAME1);
+                assertThat(md1.getExtras()).isNotNull();
+                assertThat(md1.getExtras().getString(MD_EXTRA_KEY)).isEqualTo(MD_EXTRA_VALUE);
+                assertThat(md1.getAvailableTransports()).hasSize(1);
+                assertThat(md1.getAvailableTransports()).containsExactly(
+                        PROJECTION_TRANSPORT_USB);
+
+                MobileDevice md2 = status.getConnectedMobileDevices().get(1);
+                assertThat(md2.getId()).isEqualTo(MD_ID2);
+                assertThat(md2.getName()).isEqualTo(MD_NAME2);
+                assertThat(md2.getExtras()).isNotNull();
+                assertThat(md2.getExtras().isEmpty()).isTrue();
+                assertThat(md2.getAvailableTransports()).containsExactly(
+                        PROJECTION_TRANSPORT_USB, PROJECTION_TRANSPORT_WIFI);
+
+                latch.countDown();
+            }
+        });
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void updateProjectionStatus_subscribeBeforeUpdate() throws Exception {
+
+        // We will receive notification twice - with default value and with updated one.
+        final CountDownLatch latch = new CountDownLatch(2);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                if (latch.getCount() == 2) {
+                    assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+                    assertThat(activeProjectionPackageName).isNull();
+                } else {
+                    assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+                    assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+                }
+
+                latch.countDown();
+            }
+        });
+        mService.updateProjectionStatus(createProjectionStatus(), mToken);
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void getProjectionOptions_defaults() {
+        when(mContext.getResources()).thenReturn(mResources);
+        final int uiMode = ProjectionOptions.UI_MODE_FULL_SCREEN;
+
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+                .thenReturn(uiMode);
+        when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+                .thenReturn("");
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+                .thenReturn(-1);
+        when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+                .thenReturn(new int[0]);
+
+        Bundle bundle = mService.getProjectionOptions();
+
+        ProjectionOptions options = new ProjectionOptions(bundle);
+        assertThat(options.getActivityOptions()).isNull();
+        assertThat(options.getConsentActivity()).isNull();
+        assertThat(options.getUiMode()).isEqualTo(uiMode);
+    }
+
+    @Test
+    public void getProjectionOptions_nonDefaults() {
+        when(mContext.getResources()).thenReturn(mResources);
+        final int uiMode = ProjectionOptions.UI_MODE_BLENDED;
+        final String consentActivity = "com.my.app/.MyActivity";
+        final int[] bounds = new int[] {1, 2, 3, 4};
+        final int displayId = 1;
+
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+                .thenReturn(uiMode);
+        when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+                .thenReturn(consentActivity);
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+                .thenReturn(displayId);
+        when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+                .thenReturn(bounds);
+
+        Bundle bundle = mService.getProjectionOptions();
+
+        ProjectionOptions options = new ProjectionOptions(bundle);
+        assertThat(options.getActivityOptions().getLaunchDisplayId()).isEqualTo(displayId);
+        assertThat(options.getActivityOptions().getLaunchBounds())
+                .isEqualTo(new Rect(bounds[0], bounds[1], bounds[2], bounds[3]));
+        assertThat(options.getConsentActivity()).isEqualTo(
+                ComponentName.unflattenFromString(consentActivity));
+        assertThat(options.getUiMode()).isEqualTo(uiMode);
+    }
+
+    @Test
+    public void getWifiChannels() {
+        int[] wifiChannels = mService.getAvailableWifiChannels(WifiScanner.WIFI_BAND_BOTH_WITH_DFS);
+        assertThat(wifiChannels).isNotNull();
+        assertThat(wifiChannels).isNotEmpty();
+    }
+
+    private ProjectionStatus createProjectionStatus() {
+        Bundle statusExtra = new Bundle();
+        statusExtra.putString(STATUS_EXTRA_KEY, STATUS_EXTRA_VALUE);
+        Bundle mdExtra = new Bundle();
+        mdExtra.putString(MD_EXTRA_KEY, MD_EXTRA_VALUE);
+
+        return ProjectionStatus
+                .builder(mContext.getPackageName(), PROJECTION_STATE_ACTIVE_FOREGROUND)
+                .setExtras(statusExtra)
+                .setProjectionTransport(PROJECTION_TRANSPORT_WIFI)
+                .addMobileDevice(MobileDevice
+                        .builder(MD_ID1, MD_NAME1)
+                        .addTransport(PROJECTION_TRANSPORT_USB)
+                        .setExtras(mdExtra)
+                        .build())
+                .addMobileDevice(MobileDevice
+                        .builder(MD_ID2, MD_NAME2)
+                        .addTransport(PROJECTION_TRANSPORT_USB)
+                        .addTransport(PROJECTION_TRANSPORT_WIFI)
+                        .setProjecting(true)
+                        .build())
+                .build();
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
index 3e9eb56..baaab26 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
@@ -15,153 +15,859 @@
  */
 package com.android.car.hal;
 
-import static org.mockito.ArgumentMatchers.eq;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.car.vms.IVmsPublisherClient;
+import android.car.vms.IVmsPublisherService;
+import android.car.vms.IVmsSubscriberClient;
+import android.car.vms.IVmsSubscriberService;
 import android.car.vms.VmsAssociatedLayer;
 import android.car.vms.VmsAvailableLayers;
 import android.car.vms.VmsLayer;
 import android.car.vms.VmsLayerDependency;
 import android.car.vms.VmsLayersOffering;
+import android.car.vms.VmsSubscriptionState;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
 import android.os.Binder;
 import android.os.IBinder;
 
 import androidx.test.runner.AndroidJUnit4;
 
-import com.google.android.collect.Sets;
-
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 @RunWith(AndroidJUnit4.class)
 public class VmsHalServiceTest {
-    @Rule public MockitoRule mockito = MockitoJUnit.rule();
-    @Mock private VehicleHal mMockVehicleHal;
-    @Mock private VmsHalService.VmsHalSubscriberListener mMockHalSusbcriber;
+    private static final int LAYER_TYPE = 1;
+    private static final int LAYER_SUBTYPE = 2;
+    private static final int LAYER_VERSION = 3;
+    private static final VmsLayer LAYER = new VmsLayer(LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION);
+    private static final int PUBLISHER_ID = 12345;
+    private static final byte[] PAYLOAD = new byte[]{1, 2, 3, 4};
+    private static final List<Byte> PAYLOAD_AS_LIST = Arrays.asList(new Byte[]{1, 2, 3, 4});
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private VehicleHal mVehicleHal;
+    @Mock
+    private IVmsPublisherService mPublisherService;
+    @Mock
+    private IVmsSubscriberService mSubscriberService;
+
     private IBinder mToken;
     private VmsHalService mHalService;
+    private IVmsPublisherClient mPublisherClient;
+    private IVmsSubscriberClient mSubscriberClient;
 
     @Before
     public void setUp() throws Exception {
+        mHalService = new VmsHalService(mVehicleHal);
+        mHalService.setVmsSubscriberService(mSubscriberService);
+
         mToken = new Binder();
-        mHalService = new VmsHalService(mMockVehicleHal);
-        mHalService.addSubscriberListener(mMockHalSusbcriber);
+        mPublisherClient = IVmsPublisherClient.Stub.asInterface(mHalService.getPublisherClient());
+        mPublisherClient.setVmsPublisherService(mToken, mPublisherService);
+
+        VehiclePropConfig propConfig = new VehiclePropConfig();
+        propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
+        mHalService.takeSupportedProperties(Collections.singleton(propConfig));
+
+        when(mSubscriberService.getAvailableLayers()).thenReturn(
+                new VmsAvailableLayers(Collections.emptySet(), 0));
+        mHalService.init();
+        waitForHandlerCompletion();
+
+        ArgumentCaptor<IVmsSubscriberClient> subscriberCaptor = ArgumentCaptor.forClass(
+                IVmsSubscriberClient.class);
+        verify(mSubscriberService).addVmsSubscriberToNotifications(subscriberCaptor.capture());
+        mSubscriberClient = subscriberCaptor.getValue();
+        reset(mSubscriberService);
+        verify(mVehicleHal).set(createHalMessage(
+                VmsMessageType.AVAILABILITY_CHANGE, // Message type
+                0,                                  // Sequence number
+                0));                                // # of associated layers
+        reset(mVehicleHal);
     }
 
     @Test
-    public void testSetPublisherLayersOffering() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        mHalService.setPublisherLayersOffering(mToken, offering);
+    public void testTakeSupportedProperties() {
+        VehiclePropConfig vmsPropConfig = new VehiclePropConfig();
+        vmsPropConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
 
-        VmsAssociatedLayer associatedLayer = new VmsAssociatedLayer(layer, Sets.newHashSet(12345));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(associatedLayer),
-                1)));
+        VehiclePropConfig otherPropConfig = new VehiclePropConfig();
+        otherPropConfig.prop = VehicleProperty.CURRENT_GEAR;
+
+        assertEquals(Collections.singleton(vmsPropConfig),
+                mHalService.takeSupportedProperties(Arrays.asList(otherPropConfig, vmsPropConfig)));
+    }
+
+    /**
+     * DATA message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * <li>Payload
+     * </ul>
+     */
+    @Test
+    public void testHandleDataEvent() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.DATA,                       // Message type
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
+                PUBLISHER_ID                               // PublisherId
+        );
+        message.value.bytes.addAll(PAYLOAD_AS_LIST);
+
+        sendHalMessage(message);
+        verify(mPublisherService).publish(mToken, LAYER, PUBLISHER_ID, PAYLOAD);
+    }
+
+    /**
+     * SUBSCRIBE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     */
+    @Test
+    public void testHandleSubscribeEvent() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.SUBSCRIBE,                 // Message type
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION  // VmsLayer
+        );
+
+        sendHalMessage(message);
+        verify(mSubscriberService).addVmsSubscriber(mSubscriberClient, LAYER);
+    }
+
+    /**
+     * SUBSCRIBE_TO_PUBLISHER message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * </ul>
+     */
+    @Test
+    public void testHandleSubscribeToPublisherEvent() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.SUBSCRIBE_TO_PUBLISHER,     // Message type
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
+                PUBLISHER_ID                               // PublisherId
+        );
+
+        sendHalMessage(message);
+        verify(mSubscriberService).addVmsSubscriberToPublisher(mSubscriberClient, LAYER,
+                PUBLISHER_ID);
+    }
+
+    /**
+     * UNSUBSCRIBE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     */
+    @Test
+    public void testHandleUnsubscribeEvent() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.UNSUBSCRIBE,               // Message type
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION  // VmsLayer
+        );
+
+        sendHalMessage(message);
+        verify(mSubscriberService).removeVmsSubscriber(mSubscriberClient, LAYER);
+    }
+
+    /**
+     * UNSUBSCRIBE_TO_PUBLISHER message format:
+     * <ul>
+     * <li>Message type
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Publisher ID
+     * </ul>
+     */
+    @Test
+    public void testHandleUnsubscribeFromPublisherEvent() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.UNSUBSCRIBE_TO_PUBLISHER,   // Message type
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
+                PUBLISHER_ID                               // PublisherId
+        );
+
+        sendHalMessage(message);
+        verify(mSubscriberService).removeVmsSubscriberToPublisher(mSubscriberClient, LAYER,
+                PUBLISHER_ID);
+    }
+
+    /**
+     * PUBLISHER_ID_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher info (bytes)
+     * </ul>
+     *
+     * PUBLISHER_ID_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * </ul>
+     */
+    @Test
+    public void testHandlePublisherIdRequestEvent() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.PUBLISHER_ID_REQUEST  // Message type
+        );
+        request.value.bytes.addAll(PAYLOAD_AS_LIST);
+
+        when(mPublisherService.getPublisherId(PAYLOAD)).thenReturn(PUBLISHER_ID);
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.PUBLISHER_ID_RESPONSE,  // Message type
+                PUBLISHER_ID                           // Publisher ID
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    /**
+     * PUBLISHER_INFORMATION_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * </ul>
+     *
+     * PUBLISHER_INFORMATION_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher info (bytes)
+     * </ul>
+     */
+    @Test
+    public void testHandlePublisherInformationRequestEvent() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.PUBLISHER_INFORMATION_REQUEST,  // Message type
+                PUBLISHER_ID                                   // Publisher ID
+        );
+
+        when(mSubscriberService.getPublisherInfo(PUBLISHER_ID)).thenReturn(PAYLOAD);
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.PUBLISHER_INFORMATION_RESPONSE  // Message type
+        );
+        response.value.bytes.addAll(PAYLOAD_AS_LIST);
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    /**
+     * OFFERING message format:
+     * <ul>
+     * <li>Message type
+     * <li>Publisher ID
+     * <li>Number of offerings.
+     * <li>Offerings (x number of offerings)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of layer dependencies.
+     * <li>Layer dependencies (x number of layer dependencies)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     * </ul>
+     * </ul>
+     */
+    @Test
+    public void testHandleOfferingEvent_ZeroOfferings() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.OFFERING,  // Message type
+                PUBLISHER_ID,             // PublisherId
+                0                         // # of offerings
+        );
+
+        sendHalMessage(message);
+        verify(mPublisherService).setLayersOffering(
+                mToken,
+                new VmsLayersOffering(Collections.emptySet(), PUBLISHER_ID));
     }
 
     @Test
-    public void testSetPublisherLayersOffering_Repeated() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        mHalService.setPublisherLayersOffering(mToken, offering);
-        mHalService.setPublisherLayersOffering(mToken, offering);
+    public void testHandleOfferingEvent_LayerOnly() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.OFFERING,                   // Message type
+                PUBLISHER_ID,                              // PublisherId
+                1,                                         // # of offerings
+                // Offered layer
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,
+                0                                          // # of dependencies
+        );
 
-        VmsAssociatedLayer associatedLayer = new VmsAssociatedLayer(layer, Sets.newHashSet(12345));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(associatedLayer),
-                1)));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(associatedLayer),
-                2)));
-
+        sendHalMessage(message);
+        verify(mPublisherService).setLayersOffering(
+                mToken,
+                new VmsLayersOffering(Collections.singleton(
+                        new VmsLayerDependency(LAYER)),
+                        PUBLISHER_ID));
     }
 
     @Test
-    public void testSetPublisherLayersOffering_MultiplePublishers() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        VmsLayersOffering offering2 = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 54321);
-        mHalService.setPublisherLayersOffering(mToken, offering);
-        mHalService.setPublisherLayersOffering(new Binder(), offering2);
+    public void testHandleOfferingEvent_LayerAndDependency() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.OFFERING,                   // Message type
+                PUBLISHER_ID,                              // PublisherId
+                1,                                         // # of offerings
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                1,                                         // # of dependencies
+                4, 5, 6                                    // Dependency layer
+        );
 
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345))),
-                1)));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345, 54321))),
-                2)));
-
+        sendHalMessage(message);
+        verify(mPublisherService).setLayersOffering(
+                mToken,
+                new VmsLayersOffering(Collections.singleton(
+                        new VmsLayerDependency(LAYER, Collections.singleton(
+                                new VmsLayer(4, 5, 6)))),
+                        PUBLISHER_ID));
     }
 
     @Test
-    public void testSetPublisherLayersOffering_MultiplePublishers_SharedToken() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        VmsLayersOffering offering2 = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 54321);
-        mHalService.setPublisherLayersOffering(mToken, offering);
-        mHalService.setPublisherLayersOffering(mToken, offering2);
+    public void testHandleOfferingEvent_MultipleLayersAndDependencies() throws Exception {
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.OFFERING,                   // Message type
+                PUBLISHER_ID,                              // PublisherId
+                3,                                         // # of offerings
+                // Offered layer #1
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                2,                                         // # of dependencies
+                4, 5, 6,                                   // Dependency layer
+                7, 8, 9,                                   // Dependency layer
+                // Offered layer #2
+                3, 2, 1,                                   // Layer
+                0,                                         // # of dependencies
+                // Offered layer #3
+                6, 5, 4,                                   // Layer
+                1,                                         // # of dependencies
+                7, 8, 9                                    // Dependency layer
+        );
 
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345))),
-                1)));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345, 54321))),
-                2)));
+        sendHalMessage(message);
+        verify(mPublisherService).setLayersOffering(
+                mToken,
+                new VmsLayersOffering(new LinkedHashSet<>(Arrays.asList(
+                        new VmsLayerDependency(LAYER, new LinkedHashSet<>(Arrays.asList(
+                                new VmsLayer(4, 5, 6),
+                                new VmsLayer(7, 8, 9)
+                        ))),
+                        new VmsLayerDependency(new VmsLayer(3, 2, 1), Collections.emptySet()),
+                        new VmsLayerDependency(new VmsLayer(6, 5, 4), Collections.singleton(
+                                new VmsLayer(7, 8, 9)
+                        )))),
+                        PUBLISHER_ID));
+    }
+
+    /**
+     * AVAILABILITY_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * </ul>
+     *
+     * AVAILABILITY_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number.
+     * <li>Number of associated layers.
+     * <li>Associated layers (x number of associated layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of publishers
+     * <li>Publisher ID (x number of publishers)
+     * </ul>
+     * </ul>
+     */
+    @Test
+    public void testHandleAvailabilityRequestEvent_ZeroLayers() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.AVAILABILITY_REQUEST  // Message type
+        );
+
+        when(mSubscriberService.getAvailableLayers()).thenReturn(
+                new VmsAvailableLayers(Collections.emptySet(), 123));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.AVAILABILITY_RESPONSE,  // Message type
+                123,                                   // Sequence number
+                0                                      // # of associated layers
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
     }
 
     @Test
-    public void testSetPublisherLayersOffering_MultiplePublishers_MultipleLayers() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayer layer2 = new VmsLayer(2, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        VmsLayersOffering offering2 = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer2)), 54321);
-        mHalService.setPublisherLayersOffering(mToken, offering);
-        mHalService.setPublisherLayersOffering(new Binder(), offering2);
+    public void testHandleAvailabilityRequestEvent_OneLayer() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.AVAILABILITY_REQUEST  // Message type
+        );
 
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345))),
-                1)));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(
-                        new VmsAssociatedLayer(layer, Sets.newHashSet(12345)),
-                        new VmsAssociatedLayer(layer2, Sets.newHashSet(54321))),
-                2)));
+        when(mSubscriberService.getAvailableLayers()).thenReturn(
+                new VmsAvailableLayers(Collections.singleton(
+                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID))), 123));
 
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.AVAILABILITY_RESPONSE,      // Message type
+                123,                                       // Sequence number
+                1,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                1,                                         // # of publisher IDs
+                PUBLISHER_ID                               // Publisher ID
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+
+    @Test
+    public void testHandleAvailabilityRequestEvent_MultipleLayers() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.AVAILABILITY_REQUEST  // Message type
+        );
+
+        when(mSubscriberService.getAvailableLayers()).thenReturn(
+                new VmsAvailableLayers(new LinkedHashSet<>(Arrays.asList(
+                        new VmsAssociatedLayer(LAYER,
+                                new LinkedHashSet<>(Arrays.asList(PUBLISHER_ID, 54321))),
+                        new VmsAssociatedLayer(new VmsLayer(3, 2, 1),
+                                Collections.emptySet()),
+                        new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
+                                Collections.singleton(99999)))),
+                        123));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.AVAILABILITY_RESPONSE,      // Message type
+                123,                                       // Sequence number
+                3,                                         // # of associated layers
+                // Associated layer #1
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                2,                                         // # of publisher IDs
+                PUBLISHER_ID,                              // Publisher ID
+                54321,                                     // Publisher ID #2
+                // Associated layer #2
+                3, 2, 1,                                   // Layer
+                0,                                         // # of publisher IDs
+                // Associated layer #3
+                6, 5, 4,                                   // Layer
+                1,                                         // # of publisher IDs
+                99999                                      // Publisher ID
+
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    /**
+     * AVAILABILITY_CHANGE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number.
+     * <li>Number of associated layers.
+     * <li>Associated layers (x number of associated layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of publishers
+     * <li>Publisher ID (x number of publishers)
+     * </ul>
+     * </ul>
+     */
+    @Test
+    public void testOnLayersAvailabilityChanged_ZeroLayers() throws Exception {
+        mSubscriberClient.onLayersAvailabilityChanged(
+                new VmsAvailableLayers(Collections.emptySet(), 123));
+
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.AVAILABILITY_CHANGE,    // Message type
+                123,                                   // Sequence number
+                0                                      // # of associated layers
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(message);
     }
 
     @Test
-    public void testSetPublisherLayersOffering_MultiplePublishers_MultipleLayers_SharedToken() {
-        VmsLayer layer = new VmsLayer(1, 2, 3);
-        VmsLayer layer2 = new VmsLayer(2, 2, 3);
-        VmsLayersOffering offering = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer)), 12345);
-        VmsLayersOffering offering2 = new VmsLayersOffering(
-                Sets.newHashSet(new VmsLayerDependency(layer2)), 54321);
-        mHalService.setPublisherLayersOffering(mToken, offering);
-        mHalService.setPublisherLayersOffering(mToken, offering2);
+    public void testOnLayersAvailabilityChanged_OneLayer() throws Exception {
+        mSubscriberClient.onLayersAvailabilityChanged(
+                new VmsAvailableLayers(Collections.singleton(
+                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID))), 123));
 
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(new VmsAssociatedLayer(layer, Sets.newHashSet(12345))),
-                1)));
-        verify(mMockHalSusbcriber).onLayersAvaiabilityChange(eq(new VmsAvailableLayers(
-                Sets.newHashSet(
-                        new VmsAssociatedLayer(layer, Sets.newHashSet(12345)),
-                        new VmsAssociatedLayer(layer2, Sets.newHashSet(54321))),
-                2)));
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.AVAILABILITY_CHANGE,        // Message type
+                123,                                       // Sequence number
+                1,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                1,                                         // # of publisher IDs
+                PUBLISHER_ID                               // Publisher ID
+        );
 
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(message);
+    }
+
+
+    @Test
+    public void testOnLayersAvailabilityChanged_MultipleLayers() throws Exception {
+        mSubscriberClient.onLayersAvailabilityChanged(
+                new VmsAvailableLayers(new LinkedHashSet<>(Arrays.asList(
+                        new VmsAssociatedLayer(LAYER,
+                                new LinkedHashSet<>(Arrays.asList(PUBLISHER_ID, 54321))),
+                        new VmsAssociatedLayer(new VmsLayer(3, 2, 1),
+                                Collections.emptySet()),
+                        new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
+                                Collections.singleton(99999)))),
+                        123));
+
+        VehiclePropValue message = createHalMessage(
+                VmsMessageType.AVAILABILITY_CHANGE,      // Message type
+                123,                                       // Sequence number
+                3,                                         // # of associated layers
+                // Associated layer #1
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                2,                                         // # of publisher IDs
+                PUBLISHER_ID,                              // Publisher ID
+                54321,                                     // Publisher ID #2
+                // Associated layer #2
+                3, 2, 1,                                   // Layer
+                0,                                         // # of publisher IDs
+                // Associated layer #3
+                6, 5, 4,                                   // Layer
+                1,                                         // # of publisher IDs
+                99999                                      // Publisher ID
+
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(message);
+    }
+
+    /**
+     * SUBSCRIPTION_REQUEST message format:
+     * <ul>
+     * <li>Message type
+     * </ul>
+     *
+     * SUBSCRIPTION_RESPONSE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number
+     * <li>Number of layers
+     * <li>Number of associated layers
+     * <li>Layers (x number of layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     * <li>Associated layers (x number of associated layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of publishers
+     * <li>Publisher ID (x number of publishers)
+     * </ul>
+     * </ul>
+     */
+    @Test
+    public void testHandleSubscriptionsRequestEvent_ZeroLayers() throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
+        );
+
+        when(mPublisherService.getSubscriptions()).thenReturn(
+                new VmsSubscriptionState(123, Collections.emptySet(), Collections.emptySet()));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_RESPONSE,  // Message type
+                123,                                    // Sequence number
+                0,                                      // # of layers
+                0                                       // # of associated layers
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testHandleSubscriptionsRequestEvent_OneLayer_ZeroAssociatedLayers()
+            throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
+        );
+
+        when(mPublisherService.getSubscriptions()).thenReturn(
+                new VmsSubscriptionState(123, Collections.singleton(LAYER),
+                        Collections.emptySet()));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
+                123,                                       // Sequence number
+                1,                                         // # of layers
+                0,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION   // Layer
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testHandleSubscriptionsRequestEvent_ZeroLayers_OneAssociatedLayer()
+            throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
+        );
+
+        when(mPublisherService.getSubscriptions()).thenReturn(
+                new VmsSubscriptionState(123, Collections.emptySet(), Collections.singleton(
+                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID)))));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
+                123,                                       // Sequence number
+                0,                                         // # of layers
+                1,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                1,                                         // # of publisher IDs
+                PUBLISHER_ID                               // Publisher ID
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testHandleSubscriptionsRequestEvent_MultipleLayersAndAssociatedLayers()
+            throws Exception {
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
+        );
+
+        when(mPublisherService.getSubscriptions()).thenReturn(
+                new VmsSubscriptionState(123,
+                        new LinkedHashSet<>(Arrays.asList(
+                                LAYER,
+                                new VmsLayer(4, 5, 6),
+                                new VmsLayer(7, 8, 9)
+                        )),
+                        new LinkedHashSet<>(Arrays.asList(
+                                new VmsAssociatedLayer(LAYER, Collections.emptySet()),
+                                new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
+                                        new LinkedHashSet<>(Arrays.asList(
+                                                PUBLISHER_ID,
+                                                54321))))))
+        );
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
+                123,                                       // Sequence number
+                3,                                         // # of layers
+                2,                                         // # of associated layers
+                // Layer #1
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                // Layer #2
+                4, 5, 6,                                   // Layer
+                // Layer #3
+                7, 8, 9,                                   // Layer
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                0,                                         // # of publisher IDs
+                6, 5, 4,                                   // Layer
+                2,                                         // # of publisher IDs
+                PUBLISHER_ID,                              // Publisher ID
+                54321                                      // Publisher ID #2
+        );
+
+        sendHalMessage(request);
+        verify(mVehicleHal).set(response);
+    }
+
+    /**
+     * SUBSCRIPTIONS_CHANGE message format:
+     * <ul>
+     * <li>Message type
+     * <li>Sequence number
+     * <li>Number of layers
+     * <li>Number of associated layers
+     * <li>Layers (x number of layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * </ul>
+     * <li>Associated layers (x number of associated layers)
+     * <ul>
+     * <li>Layer ID
+     * <li>Layer subtype
+     * <li>Layer version
+     * <li>Number of publishers
+     * <li>Publisher ID (x number of publishers)
+     * </ul>
+     * </ul>
+     */
+    @Test
+    public void testOnVmsSubscriptionChange_ZeroLayers() throws Exception {
+        mPublisherClient.onVmsSubscriptionChange(
+                new VmsSubscriptionState(123, Collections.emptySet(), Collections.emptySet()));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_CHANGE,    // Message type
+                123,                                    // Sequence number
+                0,                                      // # of layers
+                0                                       // # of associated layers
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testOnVmsSubscriptionChange_OneLayer_ZeroAssociatedLayers()
+            throws Exception {
+        mPublisherClient.onVmsSubscriptionChange(
+                new VmsSubscriptionState(123, Collections.singleton(LAYER),
+                        Collections.emptySet()));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
+                123,                                       // Sequence number
+                1,                                         // # of layers
+                0,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION   // Layer
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testOnVmsSubscriptionChange_ZeroLayers_OneAssociatedLayer()
+            throws Exception {
+        mPublisherClient.onVmsSubscriptionChange(
+                new VmsSubscriptionState(123, Collections.emptySet(), Collections.singleton(
+                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID)))));
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
+                123,                                       // Sequence number
+                0,                                         // # of layers
+                1,                                         // # of associated layers
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                1,                                         // # of publisher IDs
+                PUBLISHER_ID                               // Publisher ID
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(response);
+    }
+
+    @Test
+    public void testOnVmsSubscriptionChange_MultipleLayersAndAssociatedLayers()
+            throws Exception {
+        mPublisherClient.onVmsSubscriptionChange(
+                new VmsSubscriptionState(123,
+                        new LinkedHashSet<>(Arrays.asList(
+                                LAYER,
+                                new VmsLayer(4, 5, 6),
+                                new VmsLayer(7, 8, 9)
+                        )),
+                        new LinkedHashSet<>(Arrays.asList(
+                                new VmsAssociatedLayer(LAYER, Collections.emptySet()),
+                                new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
+                                        new LinkedHashSet<>(Arrays.asList(
+                                                PUBLISHER_ID,
+                                                54321))))))
+        );
+
+        VehiclePropValue response = createHalMessage(
+                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
+                123,                                       // Sequence number
+                3,                                         // # of layers
+                2,                                         // # of associated layers
+                // Layer #1
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                // Layer #2
+                4, 5, 6,                                   // Layer
+                // Layer #3
+                7, 8, 9,                                   // Layer
+                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
+                0,                                         // # of publisher IDs
+                6, 5, 4,                                   // Layer
+                2,                                         // # of publisher IDs
+                PUBLISHER_ID,                              // Publisher ID
+                54321                                      // Publisher ID #2
+        );
+
+        waitForHandlerCompletion();
+        verify(mVehicleHal).set(response);
+    }
+
+    private static VehiclePropValue createHalMessage(Integer... message) {
+        VehiclePropValue result = new VehiclePropValue();
+        result.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
+        result.value.int32Values.addAll(Arrays.asList(message));
+        return result;
+    }
+
+    private void sendHalMessage(VehiclePropValue message) {
+        mHalService.handleHalEvents(Collections.singletonList(message));
+    }
+
+    private void waitForHandlerCompletion() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mHalService.getHandler().post(() -> {
+            latch.countDown();
+        });
+        latch.await(5, TimeUnit.SECONDS);
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
index cd0e2a0..a1ff59f 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -305,9 +305,13 @@
         connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
+        verify(mContext).unbindService(connection);
         verify(mConnectionListener).onClientDisconnected(
                 eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
                         + ".VmsSystemClient U=0"));
+
+        Thread.sleep(10);
+        verifySystemBind(1);
     }
 
     @Test
@@ -319,7 +323,11 @@
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceDisconnected(null);
 
+        verify(mContext).unbindService(connection);
         verifyZeroInteractions(mConnectionListener);
+
+        Thread.sleep(10);
+        verifySystemBind(1);
     }
 
     @Test
@@ -332,9 +340,13 @@
         connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
+        verify(mContext).unbindService(connection);
         verify(mConnectionListener).onClientDisconnected(
                 eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
                         + ".VmsUserClient U=10"));
+
+        Thread.sleep(10);
+        verifyUserBind(1);
     }
 
     @Test
@@ -346,7 +358,11 @@
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceDisconnected(null);
 
+        verify(mContext).unbindService(connection);
         verifyZeroInteractions(mConnectionListener);
+
+        Thread.sleep(10);
+        verifyUserBind(1);
     }
 
     @Test
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index 78ea244..ef12ab4 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -34,6 +34,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.sysprop.CarProperties;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -636,10 +637,17 @@
     }
 
     /**
-     * Checks if the foreground user can switch to other users.
+     * Returns whether the foreground user can switch to other users.
+     *
+     * <p>For instance switching users is not allowed if the current user is in a phone call,
+     * or {@link #{UserManager.DISALLOW_USER_SWITCH} is set.
      */
     public boolean canForegroundUserSwitchUsers() {
-        return !foregroundUserHasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+        boolean inIdleCallState = TelephonyManager.getDefault().getCallState()
+                == TelephonyManager.CALL_STATE_IDLE;
+        boolean disallowUserSwitching =
+                foregroundUserHasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+        return (inIdleCallState && !disallowUserSwitching);
     }
 
     // Current process user information accessors
@@ -717,10 +725,17 @@
     }
 
     /**
-     * Checks if the user running the current process is allowed to switch to another user.
+     * Returns whether the current process user can switch to other users.
+     *
+     * <p>For instance switching users is not allowed if the user is in a phone call,
+     * or {@link #{UserManager.DISALLOW_USER_SWITCH} is set.
      */
     public boolean canCurrentProcessSwitchUsers() {
-        return !isCurrentProcessUserHasRestriction(UserManager.DISALLOW_USER_SWITCH);
+        boolean inIdleCallState = TelephonyManager.getDefault().getCallState()
+                == TelephonyManager.CALL_STATE_IDLE;
+        boolean disallowUserSwitching =
+                isCurrentProcessUserHasRestriction(UserManager.DISALLOW_USER_SWITCH);
+        return (inIdleCallState && !disallowUserSwitching);
     }
 
     /**