Merge Android10 QPR1 into AOSP master am: d114851ae9
am: 3f5a4263b7

Change-Id: Ie070e9aac2814c65c229c322f21a99e5bd173c26
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 67a00e3..85afce4 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -24,6 +24,8 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemApi;
+import android.app.Activity;
+import android.app.Service;
 import android.car.cluster.CarInstrumentClusterManager;
 import android.car.cluster.ClusterActivityState;
 import android.car.content.pm.CarPackageManager;
@@ -47,6 +49,7 @@
 import android.car.vms.VmsSubscriberManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
@@ -680,6 +683,8 @@
 
     private final Context mContext;
 
+    private final Exception mConstructionStack;
+
     private final Object mLock = new Object();
 
     @GuardedBy("mLock")
@@ -725,11 +730,10 @@
                 mConnectionState = STATE_CONNECTED;
                 mService = newService;
             }
-            if (mServiceConnectionListenerClient != null) {
-                mServiceConnectionListenerClient.onServiceConnected(name, service);
-            }
             if (mStatusChangeCallback != null) {
                 mStatusChangeCallback.onLifecycleChanged(Car.this, true);
+            } else if (mServiceConnectionListenerClient != null) {
+                mServiceConnectionListenerClient.onServiceConnected(name, service);
             }
         }
 
@@ -742,11 +746,13 @@
                 }
                 handleCarDisconnectLocked();
             }
-            if (mServiceConnectionListenerClient != null) {
-                mServiceConnectionListenerClient.onServiceDisconnected(name);
-            }
             if (mStatusChangeCallback != null) {
                 mStatusChangeCallback.onLifecycleChanged(Car.this, false);
+            } else if (mServiceConnectionListenerClient != null) {
+                mServiceConnectionListenerClient.onServiceDisconnected(name);
+            } else {
+                // This client does not handle car service restart, so should be terminated.
+                finishClient();
             }
         }
     };
@@ -768,7 +774,9 @@
 
     /**
      * A factory method that creates Car instance for all Car API access.
-     * @param context
+     * @param context App's Context. This should not be null. If you are passing
+     *                {@link ContextWrapper}, make sure that its base Context is non-null as well.
+     *                Otherwise it will throw {@link java.lang.NullPointerException}.
      * @param serviceConnectionListener listener for monitoring service connection.
      * @param handler the handler on which the callback should execute, or null to execute on the
      * service's main thread. Note: the service connection listener will be always on the main
@@ -780,6 +788,7 @@
     @Deprecated
     public static Car createCar(Context context, ServiceConnection serviceConnectionListener,
             @Nullable Handler handler) {
+        assertNonNullContext(context);
         if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
             Log.e(TAG_CAR, "FEATURE_AUTOMOTIVE not declared while android.car is used");
             return null;
@@ -821,7 +830,9 @@
     /**
      * Creates new {@link Car} object which connected synchronously to Car Service and ready to use.
      *
-     * @param context application's context
+     * @param context App's Context. This should not be null. If you are passing
+     *                {@link ContextWrapper}, make sure that its base Context is non-null as well.
+     *                Otherwise it will throw {@link java.lang.NullPointerException}.
      * @param handler the handler on which the manager's callbacks will be executed, or null to
      * execute on the application's main thread.
      *
@@ -829,6 +840,7 @@
      */
     @Nullable
     public static Car createCar(Context context, @Nullable Handler handler) {
+        assertNonNullContext(context);
         Car car = null;
         IBinder service = null;
         boolean started = false;
@@ -842,6 +854,8 @@
             }
             if (service != null) {
                 if (!started) {  // specialization for most common case.
+                    // Do this to crash client when car service crashes.
+                    car.startCarService();
                     return car;
                 }
                 break;
@@ -904,6 +918,9 @@
      * {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} and avoid the
      * needs to check if returned {@link Car} is connected or not from returned {@link Car}.</p>
      *
+     * @param context App's Context. This should not be null. If you are passing
+     *                {@link ContextWrapper}, make sure that its base Context is non-null as well.
+     *                Otherwise it will throw {@link java.lang.NullPointerException}.
      * @param handler dispatches all Car*Manager events to this Handler. Exception is
      *                {@link CarServiceLifecycleListener} which will be always dispatched to main
      *                thread. Passing null leads into dispatching all Car*Manager callbacks to main
@@ -920,7 +937,7 @@
     public static Car createCar(@NonNull Context context,
             @Nullable Handler handler, long waitTimeoutMs,
             @NonNull CarServiceLifecycleListener statusChangeListener) {
-        Preconditions.checkNotNull(context);
+        assertNonNullContext(context);
         Preconditions.checkNotNull(statusChangeListener);
         Car car = null;
         IBinder service = null;
@@ -1001,6 +1018,15 @@
         return car;
     }
 
+    private static void assertNonNullContext(Context context) {
+        Preconditions.checkNotNull(context);
+        if (context instanceof ContextWrapper
+                && ((ContextWrapper) context).getBaseContext() == null) {
+            throw new NullPointerException(
+                    "ContextWrapper with null base passed as Context, forgot to set base Context?");
+        }
+    }
+
     private void dispatchCarReadyToMainThread(boolean isMainThread) {
         if (isMainThread) {
             mStatusChangeCallback.onLifecycleChanged(this, true);
@@ -1027,6 +1053,13 @@
         }
         mServiceConnectionListenerClient = serviceConnectionListener;
         mStatusChangeCallback = statusChangeListener;
+        // Store construction stack so that client can get help when it crashes when car service
+        // crashes.
+        if (serviceConnectionListener == null && statusChangeListener == null) {
+            mConstructionStack = new RuntimeException();
+        } else {
+            mConstructionStack = null;
+        }
     }
 
     /**
@@ -1136,12 +1169,15 @@
     @Nullable
     public Object getCarManager(String serviceName) {
         CarManagerBase manager;
-        ICar service = getICarOrThrow();
         synchronized (mLock) {
+            if (mService == null) {
+                Log.w(TAG_CAR, "getCarManager not working while car service not ready");
+                return null;
+            }
             manager = mServiceMap.get(serviceName);
             if (manager == null) {
                 try {
-                    IBinder binder = service.getCarService(serviceName);
+                    IBinder binder = mService.getCarService(serviceName);
                     if (binder == null) {
                         Log.w(TAG_CAR, "getCarManager could not get binder for service:"
                                 + serviceName);
@@ -1155,7 +1191,7 @@
                     }
                     mServiceMap.put(serviceName, manager);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             }
         }
@@ -1171,84 +1207,143 @@
         return CONNECTION_TYPE_EMBEDDED;
     }
 
+    /** @hide */
+    Context getContext() {
+        return mContext;
+    }
+
+    /** @hide */
+    Handler getEventHandler() {
+        return mEventHandler;
+    }
+
+    /** @hide */
+    <T> T handleRemoteExceptionFromCarService(RemoteException e, T returnValue) {
+        handleRemoteExceptionFromCarService(e);
+        return returnValue;
+    }
+
+    /** @hide */
+    void handleRemoteExceptionFromCarService(RemoteException e) {
+        Log.w(TAG_CAR, "Car service has crashed", e);
+    }
+
+
+    private void finishClient() {
+        if (mContext == null) {
+            throw new IllegalStateException("Car service has crashed, null Context");
+        }
+        if (mContext instanceof Activity) {
+            Activity activity = (Activity) mContext;
+            if (!activity.isFinishing()) {
+                Log.w(TAG_CAR,
+                        "Car service crashed, client not handling it, finish Activity, created "
+                                + "from " + mConstructionStack);
+                activity.finish();
+            }
+            return;
+        } else if (mContext instanceof Service) {
+            Service service = (Service) mContext;
+            throw new IllegalStateException("Car service has crashed, client not handle it:"
+                    + service.getPackageName() + "," + service.getClass().getSimpleName(),
+                    mConstructionStack);
+        }
+        throw new IllegalStateException("Car service crashed, client not handling it.",
+                mConstructionStack);
+    }
+
+    /** @hide */
+    public static <T> T handleRemoteExceptionFromCarService(Service service, RemoteException e,
+            T returnValue) {
+        handleRemoteExceptionFromCarService(service, e);
+        return returnValue;
+    }
+
+    /** @hide */
+    public static  void handleRemoteExceptionFromCarService(Service service, RemoteException e) {
+        Log.w(TAG_CAR,
+                "Car service has crashed, client:" + service.getPackageName() + ","
+                        + service.getClass().getSimpleName(), e);
+        service.stopSelf();
+    }
+
     @Nullable
     private CarManagerBase createCarManager(String serviceName, IBinder binder) {
         CarManagerBase manager = null;
         switch (serviceName) {
             case AUDIO_SERVICE:
-                manager = new CarAudioManager(binder, mContext, mEventHandler);
+                manager = new CarAudioManager(this, binder);
                 break;
             case SENSOR_SERVICE:
-                manager = new CarSensorManager(binder, mContext, mEventHandler);
+                manager = new CarSensorManager(this, binder);
                 break;
             case INFO_SERVICE:
-                manager = new CarInfoManager(binder);
+                manager = new CarInfoManager(this, binder);
                 break;
             case APP_FOCUS_SERVICE:
-                manager = new CarAppFocusManager(binder, mEventHandler);
+                manager = new CarAppFocusManager(this, binder);
                 break;
             case PACKAGE_SERVICE:
-                manager = new CarPackageManager(binder, mContext);
+                manager = new CarPackageManager(this, binder);
                 break;
             case CAR_NAVIGATION_SERVICE:
-                manager = new CarNavigationStatusManager(binder);
+                manager = new CarNavigationStatusManager(this, binder);
                 break;
             case CABIN_SERVICE:
-                manager = new CarCabinManager(binder, mContext, mEventHandler);
+                manager = new CarCabinManager(this, binder);
                 break;
             case DIAGNOSTIC_SERVICE:
-                manager = new CarDiagnosticManager(binder, mContext, mEventHandler);
+                manager = new CarDiagnosticManager(this, binder);
                 break;
             case HVAC_SERVICE:
-                manager = new CarHvacManager(binder, mContext, mEventHandler);
+                manager = new CarHvacManager(this, binder);
                 break;
             case POWER_SERVICE:
-                manager = new CarPowerManager(binder, mContext, mEventHandler);
+                manager = new CarPowerManager(this, binder);
                 break;
             case PROJECTION_SERVICE:
-                manager = new CarProjectionManager(binder, mEventHandler);
+                manager = new CarProjectionManager(this, binder);
                 break;
             case PROPERTY_SERVICE:
-                manager = new CarPropertyManager(ICarProperty.Stub.asInterface(binder),
-                    mEventHandler);
+                manager = new CarPropertyManager(this, ICarProperty.Stub.asInterface(binder));
                 break;
             case VENDOR_EXTENSION_SERVICE:
-                manager = new CarVendorExtensionManager(binder, mEventHandler);
+                manager = new CarVendorExtensionManager(this, binder);
                 break;
             case CAR_INSTRUMENT_CLUSTER_SERVICE:
-                manager = new CarInstrumentClusterManager(binder, mEventHandler);
+                manager = new CarInstrumentClusterManager(this, binder);
                 break;
             case TEST_SERVICE:
                 /* CarTestManager exist in static library. So instead of constructing it here,
                  * only pass binder wrapper so that CarTestManager can be constructed outside. */
-                manager = new CarTestManagerBinderWrapper(binder);
+                manager = new CarTestManagerBinderWrapper(this, binder);
                 break;
             case VMS_SUBSCRIBER_SERVICE:
-                manager = new VmsSubscriberManager(binder);
+                manager = new VmsSubscriberManager(this, binder);
                 break;
             case BLUETOOTH_SERVICE:
-                manager = new CarBluetoothManager(binder, mContext);
+                manager = new CarBluetoothManager(this, binder);
                 break;
             case STORAGE_MONITORING_SERVICE:
-                manager = new CarStorageMonitoringManager(binder, mEventHandler);
+                manager = new CarStorageMonitoringManager(this, binder);
                 break;
             case CAR_DRIVING_STATE_SERVICE:
-                manager = new CarDrivingStateManager(binder, mContext, mEventHandler);
+                manager = new CarDrivingStateManager(this, binder);
                 break;
             case CAR_UX_RESTRICTION_SERVICE:
-                manager = new CarUxRestrictionsManager(binder, mContext, mEventHandler);
+                manager = new CarUxRestrictionsManager(this, binder);
                 break;
             case CAR_CONFIGURATION_SERVICE:
-                manager = new CarConfigurationManager(binder);
+                manager = new CarConfigurationManager(this, binder);
                 break;
             case CAR_TRUST_AGENT_ENROLLMENT_SERVICE:
-                manager = new CarTrustAgentEnrollmentManager(binder, mContext, mEventHandler);
+                manager = new CarTrustAgentEnrollmentManager(this, binder);
                 break;
             case CAR_MEDIA_SERVICE:
-                manager = new CarMediaManager(binder);
+                manager = new CarMediaManager(this, binder);
                 break;
             case CAR_BUGREPORT_SERVICE:
-                manager = new CarBugreportManager(binder, mContext);
+                manager = new CarBugreportManager(this, binder);
                 break;
             default:
                 break;
@@ -1281,15 +1376,6 @@
         }
     }
 
-    private ICar getICarOrThrow() throws IllegalStateException {
-        synchronized (mLock) {
-            if (mService == null) {
-                throw new IllegalStateException("not connected");
-            }
-            return mService;
-        }
-    }
-
     private void tearDownCarManagersLocked() {
         // All disconnected handling should be only doing its internal cleanup.
         for (CarManagerBase manager: mServiceMap.values()) {
diff --git a/car-lib/src/android/car/CarAppFocusManager.java b/car-lib/src/android/car/CarAppFocusManager.java
index ff0c25d..b8936af 100644
--- a/car-lib/src/android/car/CarAppFocusManager.java
+++ b/car-lib/src/android/car/CarAppFocusManager.java
@@ -17,7 +17,6 @@
 package android.car;
 
 import android.annotation.IntDef;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 
@@ -35,7 +34,7 @@
  * should run in the system, and other app setting the flag for the matching app should
  * lead into other app to stop.
  */
-public final class CarAppFocusManager implements CarManagerBase {
+public final class CarAppFocusManager extends CarManagerBase {
     /**
      * Listener to get notification for app getting information on application type status changes.
      */
@@ -114,7 +113,6 @@
     public @interface AppFocusRequestResult {}
 
     private final IAppFocus mService;
-    private final Handler mHandler;
     private final Map<OnAppFocusChangedListener, IAppFocusListenerImpl> mChangeBinders =
             new HashMap<>();
     private final Map<OnAppFocusOwnershipCallback, IAppFocusOwnershipCallbackImpl>
@@ -123,9 +121,9 @@
     /**
      * @hide
      */
-    CarAppFocusManager(IBinder service, Handler handler) {
+    CarAppFocusManager(Car car, IBinder service) {
+        super(car);
         mService = IAppFocus.Stub.asInterface(service);
-        mHandler = handler;
     }
 
     /**
@@ -149,7 +147,7 @@
         try {
             mService.registerFocusListener(binder, appType);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -169,7 +167,8 @@
         try {
             mService.unregisterFocusListener(binder, appType);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
+            // continue for local clean-up
         }
         synchronized (this) {
             binder.removeAppType(appType);
@@ -197,7 +196,7 @@
                 mService.unregisterFocusListener(binder, appType);
             }
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -209,7 +208,7 @@
         try {
             return mService.getActiveAppTypes();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, new int[0]);
         }
     }
 
@@ -229,7 +228,7 @@
         try {
             return mService.isOwningFocus(binder, appType);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -261,7 +260,7 @@
         try {
             return mService.requestAppFocus(binder, appType);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, APP_FOCUS_REQUEST_FAILED);
         }
     }
 
@@ -286,7 +285,8 @@
         try {
             mService.abandonAppFocus(binder, appType);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
+            // continue for local clean-up
         }
         synchronized (this) {
             binder.removeAppType(appType);
@@ -314,7 +314,7 @@
                 mService.abandonAppFocus(binder, appType);
             }
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -359,11 +359,8 @@
             if (manager == null || listener == null) {
                 return;
             }
-            manager.mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    listener.onAppFocusChanged(appType, active);
-                }
+            manager.getEventHandler().post(() -> {
+                listener.onAppFocusChanged(appType, active);
             });
         }
     }
@@ -403,11 +400,8 @@
             if (manager == null || callback == null) {
                 return;
             }
-            manager.mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    callback.onAppFocusOwnershipLost(appType);
-                }
+            manager.getEventHandler().post(() -> {
+                callback.onAppFocusOwnershipLost(appType);
             });
         }
 
@@ -418,11 +412,8 @@
             if (manager == null || callback == null) {
                 return;
             }
-            manager.mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    callback.onAppFocusOwnershipGranted(appType);
-                }
+            manager.getEventHandler().post(() -> {
+                callback.onAppFocusOwnershipGranted(appType);
             });
         }
     }
diff --git a/car-lib/src/android/car/CarBluetoothManager.java b/car-lib/src/android/car/CarBluetoothManager.java
index c85cec7..c8e46ca 100644
--- a/car-lib/src/android/car/CarBluetoothManager.java
+++ b/car-lib/src/android/car/CarBluetoothManager.java
@@ -17,7 +17,6 @@
 
 import android.Manifest;
 import android.annotation.RequiresPermission;
-import android.content.Context;
 import android.os.IBinder;
 import android.os.RemoteException;
 
@@ -26,9 +25,8 @@
  *
  * @hide
  */
-public final class CarBluetoothManager implements CarManagerBase {
+public final class CarBluetoothManager extends CarManagerBase {
     private static final String TAG = "CarBluetoothManager";
-    private final Context mContext;
     private final ICarBluetooth mService;
 
     /**
@@ -50,7 +48,7 @@
         try {
             mService.connectDevices();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -59,8 +57,8 @@
      *
      * @hide
      */
-    public CarBluetoothManager(IBinder service, Context context) {
-        mContext = context;
+    public CarBluetoothManager(Car car, IBinder service) {
+        super(car);
         mService = ICarBluetooth.Stub.asInterface(service);
     }
 
diff --git a/car-lib/src/android/car/CarBugreportManager.java b/car-lib/src/android/car/CarBugreportManager.java
index f5d3b1d..99f2c7c 100644
--- a/car-lib/src/android/car/CarBugreportManager.java
+++ b/car-lib/src/android/car/CarBugreportManager.java
@@ -20,7 +20,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
-import android.content.Context;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
@@ -39,10 +38,9 @@
  *
  * @hide
  */
-public final class CarBugreportManager implements CarManagerBase {
+public final class CarBugreportManager extends CarManagerBase {
 
     private final ICarBugreportService mService;
-    private Handler mHandler;
 
     /**
      * Callback from carbugreport manager. Callback methods are always called on the main thread.
@@ -153,9 +151,9 @@
      *
      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
      */
-    public CarBugreportManager(IBinder service, Context context) {
+    public CarBugreportManager(Car car, IBinder service) {
+        super(car);
         mService = ICarBugreportService.Stub.asInterface(service);
-        mHandler = new Handler(context.getMainLooper());
     }
 
     /**
@@ -185,10 +183,10 @@
         Preconditions.checkNotNull(callback);
         try {
             CarBugreportManagerCallbackWrapper wrapper =
-                    new CarBugreportManagerCallbackWrapper(callback, mHandler);
+                    new CarBugreportManagerCallbackWrapper(callback, getEventHandler());
             mService.requestBugreport(output, extraOutput, wrapper);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         } finally {
             IoUtils.closeQuietly(output);
             IoUtils.closeQuietly(extraOutput);
diff --git a/car-lib/src/android/car/CarInfoManager.java b/car-lib/src/android/car/CarInfoManager.java
index bf09495..e9a170d 100644
--- a/car-lib/src/android/car/CarInfoManager.java
+++ b/car-lib/src/android/car/CarInfoManager.java
@@ -29,7 +29,7 @@
  * Utility to retrieve various static information from car. Each data are grouped as {@link Bundle}
  * and relevant data can be checked from {@link Bundle} using pre-specified keys.
  */
-public final class CarInfoManager implements CarManagerBase{
+public final class CarInfoManager extends CarManagerBase {
 
     private final CarPropertyManager mCarPropertyMgr;
     /**
@@ -261,9 +261,10 @@
     }
 
     /** @hide */
-    CarInfoManager(IBinder service) {
+    CarInfoManager(Car car, IBinder service) {
+        super(car);
         ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
-        mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, null);
+        mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
     }
 
     /** @hide */
diff --git a/car-lib/src/android/car/CarManagerBase.java b/car-lib/src/android/car/CarManagerBase.java
index 737f356..8d30fdf 100644
--- a/car-lib/src/android/car/CarManagerBase.java
+++ b/car-lib/src/android/car/CarManagerBase.java
@@ -16,10 +16,44 @@
 
 package android.car;
 
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+
 /**
- * Common interface for Car*Manager
+ * Common base class for Car*Manager
  * @hide
  */
-public interface CarManagerBase {
-    void onCarDisconnected();
+public abstract class CarManagerBase {
+
+    protected final Car mCar;
+
+    public CarManagerBase(Car car) {
+        mCar = car;
+    }
+
+    protected Context getContext() {
+        return mCar.getContext();
+    }
+
+    protected Handler getEventHandler() {
+        return mCar.getEventHandler();
+    }
+
+    protected <T> T handleRemoteExceptionFromCarService(RemoteException e, T returnValue) {
+        return mCar.handleRemoteExceptionFromCarService(e, returnValue);
+    }
+
+    protected void handleRemoteExceptionFromCarService(RemoteException e) {
+        mCar.handleRemoteExceptionFromCarService(e);
+    }
+
+    /**
+     * Handle disconnection of car service (=crash). As car service has crashed already, this call
+     * should only clean up any listeners / callbacks passed from client. Clearing object passed
+     * to car service is not necessary as car service has crashed. Note that Car*Manager will not
+     * work any more as all binders are invalid. Client should re-create all Car*Managers when
+     * car service is restarted.
+     */
+    protected abstract void onCarDisconnected();
 }
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 4c177f7..bc4107f 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -69,7 +69,7 @@
  * @hide
  */
 @SystemApi
-public final class CarProjectionManager implements CarManagerBase {
+public final class CarProjectionManager extends CarManagerBase {
     private static final String TAG = CarProjectionManager.class.getSimpleName();
 
     private final Binder mToken = new Binder();
@@ -194,7 +194,6 @@
     public static final int PROJECTION_AP_FAILED = 2;
 
     private final ICarProjection mService;
-    private final Handler mHandler;
     private final Executor mHandlerExecutor;
 
     @GuardedBy("mLock")
@@ -241,9 +240,10 @@
     /**
      * @hide
      */
-    public CarProjectionManager(IBinder service, Handler handler) {
+    public CarProjectionManager(Car car, IBinder service) {
+        super(car);
         mService = ICarProjection.Stub.asInterface(service);
-        mHandler = handler;
+        Handler handler = getEventHandler();
         mHandlerExecutor = handler::post;
     }
 
@@ -448,7 +448,8 @@
                 mService.unregisterKeyEventHandler(mBinderHandler);
             }
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
+            return;
         }
 
         mHandledEvents = events;
@@ -466,7 +467,7 @@
             try {
                 mService.registerProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                handleRemoteExceptionFromCarService(e);
             }
         }
     }
@@ -483,7 +484,7 @@
             try {
                 mService.unregisterProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                handleRemoteExceptionFromCarService(e);
             }
         }
     }
@@ -507,14 +508,14 @@
     public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
         Preconditions.checkNotNull(callback, "callback cannot be null");
         synchronized (mLock) {
-            Looper looper = mHandler.getLooper();
+            Looper looper = getEventHandler().getLooper();
             ProjectionAccessPointCallbackProxy proxy =
                     new ProjectionAccessPointCallbackProxy(this, looper, callback);
             try {
                 mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken);
                 mProjectionAccessPointCallbackProxy = proxy;
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                handleRemoteExceptionFromCarService(e);
             }
         }
     }
@@ -535,7 +536,7 @@
             }
             return channelList;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
@@ -556,7 +557,7 @@
         try {
             mService.stopProjectionAccessPoint(mAccessPointProxyToken);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -575,7 +576,7 @@
         try {
             return mService.requestBluetoothProfileInhibit(device, profile, mToken);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -593,7 +594,7 @@
         try {
             return mService.releaseBluetoothProfileInhibit(device, profile, mToken);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -611,7 +612,7 @@
         try {
             mService.updateProjectionStatus(status, mToken);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -634,12 +635,12 @@
                 try {
                     mService.registerProjectionStatusListener(mCarProjectionStatusListener);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             } else {
                 // Already subscribed to Car Service, immediately notify listener with the current
                 // projection status in the event handler thread.
-                mHandler.post(() ->
+                getEventHandler().post(() ->
                         listener.onProjectionStatusChanged(
                                 mCarProjectionStatusListener.mCurrentState,
                                 mCarProjectionStatusListener.mCurrentPackageName,
@@ -671,7 +672,7 @@
             mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
             mCarProjectionStatusListener = null;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -695,7 +696,7 @@
         try {
             return mService.getProjectionOptions();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Bundle.EMPTY);
         }
     }
 
@@ -838,7 +839,7 @@
                 List<ProjectionStatus> details) {
             CarProjectionManager mgr = mManagerRef.get();
             if (mgr != null) {
-                mgr.mHandler.post(() -> {
+                mgr.getEventHandler().post(() -> {
                     mCurrentState = projectionState;
                     mCurrentPackageName = packageName;
                     mDetails = Collections.unmodifiableList(details);
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index c335e58..ada2b2d 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -338,38 +338,44 @@
     /**
      * Distance units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      */
     public static final int DISTANCE_DISPLAY_UNITS = 289408512;
     /**
      * Fuel volume units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      */
     public static final int FUEL_VOLUME_DISPLAY_UNITS = 289408513;
     /**
      * Tire pressure units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      */
     public static final int TIRE_PRESSURE_DISPLAY_UNITS = 289408514;
     /**
      * EV battery units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      */
     public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
     /**
      * Speed Units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      * @hide
      */
     public static final int VEHICLE_SPEED_DISPLAY_UNITS = 289408516;
     /**
      * Fuel consumption units for display
      * Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
-     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+     * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+     * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
      */
     public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
     /**
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
index 54d10e2..2b633a0 100644
--- a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -17,10 +17,10 @@
 package android.car.cluster;
 
 import android.annotation.SystemApi;
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 
 /**
@@ -35,7 +35,7 @@
  */
 @Deprecated
 @SystemApi
-public class CarInstrumentClusterManager implements CarManagerBase {
+public class CarInstrumentClusterManager extends CarManagerBase {
     /**
      * @deprecated use {@link android.car.Car#CATEGORY_NAVIGATION} instead
      *
@@ -101,7 +101,8 @@
     }
 
     /** @hide */
-    public CarInstrumentClusterManager(IBinder service, Handler handler) {
+    public CarInstrumentClusterManager(Car car, IBinder service) {
+        super(car);
         // No-op
     }
 
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
index 7deecc7..4f41796 100644
--- a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
@@ -28,6 +28,8 @@
     /**
      * Returns {@link IInstrumentClusterNavigation} that will be passed to the navigation
      * application.
+     *
+     * TODO(b/141992448) : remove blocking call
      */
     IInstrumentClusterNavigation getNavigationService();
 
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
new file mode 100644
index 0000000..680e241
--- /dev/null
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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.cluster.renderer;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Helper binder API for InstrumentClusterRenderingService. This contains binder calls to car
+ * service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterHelper {
+    /**
+     * Start an activity to specified display / user. The activity is considered as
+     * in fixed mode for the display and will be re-launched if the activity crashes, the package
+     * is updated or goes to background for whatever reason.
+     * Only one activity can exist in fixed mode for the target display and calling this multiple
+     * times with different {@code Intent} will lead into making all previous activities into
+     * non-fixed normal state (= will not be re-launched.)
+     *
+     * Do not change binder transaction number.
+     */
+    boolean startFixedActivityModeForDisplayAndUser(in Intent intent,
+            in Bundle activityOptionsBundle, int userId) = 0;
+    /**
+     * The activity lauched on the display is no longer in fixed mode. Re-launching or finishing
+     * should not trigger re-launfhing any more. Note that Activity for non-current user will
+     * be auto-stopped and there is no need to call this for user swiching. Note that this does not
+     * stop the activity but it will not be re-launched any more.
+     *
+     * Do not change binder transaction number.
+     */
+    void stopFixedActivityMode(int displayId) = 1;
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 6996119..ede4b6a 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
 import android.app.ActivityOptions;
 import android.app.Service;
 import android.car.Car;
@@ -82,20 +83,35 @@
  */
 @SystemApi
 public abstract class InstrumentClusterRenderingService extends Service {
+    /**
+     * Key to pass IInstrumentClusterHelper binder in onBind call {@link Intent} through extra
+     * {@link Bundle). Both extra bundle and binder itself use this key.
+     *
+     * @hide
+     */
+    public static final String EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER =
+            "android.car.cluster.renderer.IInstrumentClusterHelper";
+
     private static final String TAG = CarLibLog.TAG_CLUSTER;
 
     private static final String BITMAP_QUERY_WIDTH = "w";
     private static final String BITMAP_QUERY_HEIGHT = "h";
+    private static final String BITMAP_QUERY_OFFLANESALPHA = "offLanesAlpha";
+
+    private final Handler mUiHandler = new Handler(Looper.getMainLooper());
 
     private final Object mLock = new Object();
+    // Main thread only
     private RendererBinder mRendererBinder;
-    private Handler mUiHandler = new Handler(Looper.getMainLooper());
     private ActivityOptions mActivityOptions;
     private ClusterActivityState mActivityState;
     private ComponentName mNavigationComponent;
     @GuardedBy("mLock")
     private ContextOwner mNavContextOwner;
 
+    @GuardedBy("mLock")
+    private IInstrumentClusterHelper mInstrumentClusterHelper;
+
     private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
     private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
             IMAGE_CACHE_SIZE_BYTES) {
@@ -157,6 +173,18 @@
             Log.d(TAG, "onBind, intent: " + intent);
         }
 
+        Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+        IBinder binder = null;
+        if (bundle != null) {
+            binder = bundle.getBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+        }
+        if (binder == null) {
+            Log.wtf(TAG, "IInstrumentClusterHelper not passed through binder");
+        } else {
+            synchronized (mLock) {
+                mInstrumentClusterHelper = IInstrumentClusterHelper.Stub.asInterface(binder);
+            }
+        }
         if (mRendererBinder == null) {
             mRendererBinder = new RendererBinder(getNavigationRenderer());
         }
@@ -196,6 +224,76 @@
     public void onNavigationComponentReleased() {
     }
 
+    @Nullable
+    private IInstrumentClusterHelper getClusterHelper() {
+        synchronized (mLock) {
+            if (mInstrumentClusterHelper == null) {
+                Log.w("mInstrumentClusterHelper still null, should wait until onBind",
+                        new RuntimeException());
+            }
+            return mInstrumentClusterHelper;
+        }
+    }
+
+    /**
+     * Start Activity in fixed mode.
+     *
+     * <p>Activity launched in this way will stay visible across crash, package updatge
+     * or other Activity launch. So this should be carefully used for case like apps running
+     * in instrument cluster.</p>
+     *
+     * <p> Only one Activity can stay in this mode for a display and launching other Activity
+     * with this call means old one get out of the mode. Alternatively
+     * {@link #stopFixedActivityMode(int)} can be called to get the top activitgy out of this
+     * mode.</p>
+     *
+     * @param intent Should include specific {@code ComponentName}.
+     * @param options Should include target display.
+     * @param userId Target user id
+     * @return {@code true} if succeeded. {@code false} may mean the target component is not ready
+     *         or available. Note that failure can happen during early boot-up stage even if the
+     *         target Activity is in normal state and client should retry when it fails. Once it is
+     *         successfully launched, car service will guarantee that it is running across crash or
+     *         other events.
+     *
+     * @hide
+     */
+    protected boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
+            @NonNull ActivityOptions options, @UserIdInt int userId) {
+        IInstrumentClusterHelper helper = getClusterHelper();
+        if (helper == null) {
+            return false;
+        }
+        try {
+            return helper.startFixedActivityModeForDisplayAndUser(intent, options.toBundle(),
+                    userId);
+        } catch (RemoteException e) {
+            Log.w("Remote exception from car service", e);
+            // Probably car service will restart and rebind. So do nothing.
+        }
+        return false;
+    }
+
+
+    /**
+     * Stop fixed mode for top Activity in the display. Crashing or launching other Activity
+     * will not re-launch the top Activity any more.
+     *
+     * @hide
+     */
+    protected void stopFixedActivityMode(int displayId) {
+        IInstrumentClusterHelper helper = getClusterHelper();
+        if (helper == null) {
+            return;
+        }
+        try {
+            helper.stopFixedActivityMode(displayId);
+        } catch (RemoteException e) {
+            Log.w("Remote exception from car service, displayId:" + displayId, e);
+            // Probably car service will restart and rebind. So do nothing.
+        }
+    }
+
     /**
      * Updates the cluster navigation activity by checking which activity to show (an activity of
      * the {@link #mNavContextOwner}). If not yet launched, it will do so.
@@ -370,16 +468,19 @@
     @CallSuper
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
-        writer.println("**" + getClass().getSimpleName() + "**");
-        writer.println("renderer binder: " + mRendererBinder);
-        if (mRendererBinder != null) {
-            writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+        synchronized (mLock) {
+            writer.println("**" + getClass().getSimpleName() + "**");
+            writer.println("renderer binder: " + mRendererBinder);
+            if (mRendererBinder != null) {
+                writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+            }
+            writer.println("navigation focus owner: " + getNavigationContextOwner());
+            writer.println("activity options: " + mActivityOptions);
+            writer.println("activity state: " + mActivityState);
+            writer.println("current nav component: " + mNavigationComponent);
+            writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
+            writer.println("mInstrumentClusterHelper" + mInstrumentClusterHelper);
         }
-        writer.println("navigation focus owner: " + getNavigationContextOwner());
-        writer.println("activity options: " + mActivityOptions);
-        writer.println("activity state: " + mActivityState);
-        writer.println("current nav component: " + mNavigationComponent);
-        writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
     }
 
     private class RendererBinder extends IInstrumentCluster.Stub {
@@ -539,8 +640,18 @@
     }
 
     /**
+     * See {@link #getBitmap(Uri, int, int, float)}
+     *
+     * @throws IllegalArgumentException if {@code width} or {@code height} is not greater than 0.
+     */
+    @Nullable
+    public Bitmap getBitmap(Uri uri, int width, int height) {
+        return getBitmap(uri, width, height, 1f);
+    }
+
+    /**
      * Fetches a bitmap from the navigation context owner (application holding navigation focus)
-     * of the given width and height. The fetched bitmaps are cached.
+     * of the given width and height and off lane opacity. The fetched bitmaps are cached.
      * It returns null if:
      * <ul>
      * <li>there is no navigation context owner
@@ -549,13 +660,17 @@
      * </ul>
      * This is a costly operation. Returned bitmaps should be fetched on a secondary thread.
      *
-     * @throws IllegalArgumentException if {@code width} or {@code height} is not greater than 0.
+     * @throws IllegalArgumentException if width, height <= 0, or 0 > offLanesAlpha > 1
+     * @hide
      */
     @Nullable
-    public Bitmap getBitmap(Uri uri, int width, int height) {
+    public Bitmap getBitmap(Uri uri, int width, int height, float offLanesAlpha) {
         if (width <= 0 || height <= 0) {
             throw new IllegalArgumentException("Width and height must be > 0");
         }
+        if (offLanesAlpha < 0 || offLanesAlpha > 1) {
+            throw new IllegalArgumentException("offLanesAlpha must be between [0, 1]");
+        }
 
         try {
             ContextOwner contextOwner = getNavigationContextOwner();
@@ -567,6 +682,7 @@
             uri = uri.buildUpon()
                     .appendQueryParameter(BITMAP_QUERY_WIDTH, String.valueOf(width))
                     .appendQueryParameter(BITMAP_QUERY_HEIGHT, String.valueOf(height))
+                    .appendQueryParameter(BITMAP_QUERY_OFFLANESALPHA, String.valueOf(offLanesAlpha))
                     .build();
 
             String host = uri.getHost();
diff --git a/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java b/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
index 5b0a6bd..f95063a 100644
--- a/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
+++ b/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
@@ -17,6 +17,7 @@
 
 import android.annotation.SystemApi;
 import android.app.Service;
+import android.car.Car;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.IBinder;
@@ -76,7 +77,7 @@
             try {
                 setter.setAppBlockingPolicy(policy);
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                Car.handleRemoteExceptionFromCarService(CarAppBlockingPolicyService.this, e);
             }
         }
     }
diff --git a/car-lib/src/android/car/content/pm/CarPackageManager.java b/car-lib/src/android/car/content/pm/CarPackageManager.java
index d23633d..7498c65 100644
--- a/car-lib/src/android/car/content/pm/CarPackageManager.java
+++ b/car-lib/src/android/car/content/pm/CarPackageManager.java
@@ -19,9 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.content.ComponentName;
-import android.content.Context;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -32,7 +32,7 @@
 /**
  * Provides car specific API related with package management.
  */
-public final class CarPackageManager implements CarManagerBase {
+public final class CarPackageManager extends CarManagerBase {
     private static final String TAG = "CarPackageManager";
 
     /**
@@ -70,12 +70,11 @@
     public @interface SetPolicyFlags {}
 
     private final ICarPackageManager mService;
-    private final Context mContext;
 
     /** @hide */
-    public CarPackageManager(IBinder service, Context context) {
+    public CarPackageManager(Car car, IBinder service) {
+        super(car);
         mService = ICarPackageManager.Stub.asInterface(service);
-        mContext = context;
     }
 
     /** @hide */
@@ -115,7 +114,7 @@
         try {
             mService.setAppBlockingPolicy(packageName, policy, flags);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -128,7 +127,7 @@
         try {
             mService.restartTask(taskId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -151,7 +150,7 @@
         try {
             return mService.isActivityBackedBySafeActivity(activityName);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -165,7 +164,7 @@
         try {
             mService.setEnableActivityBlocking(enable);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -181,7 +180,7 @@
         try {
             return mService.isActivityDistractionOptimized(packageName, className);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -197,7 +196,7 @@
         try {
             return mService.isServiceDistractionOptimized(packageName, className);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 }
diff --git a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
index 1559dd4..c9c8b6f 100644
--- a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
+++ b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
@@ -23,8 +23,6 @@
 import android.car.CarLibLog;
 import android.car.CarManagerBase;
 import android.car.diagnostic.ICarDiagnosticEventListener.Stub;
-import android.content.Context;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -47,7 +45,7 @@
  * @hide
  */
 @SystemApi
-public final class CarDiagnosticManager implements CarManagerBase {
+public final class CarDiagnosticManager extends CarManagerBase {
     public static final int FRAME_TYPE_LIVE = 0;
     public static final int FRAME_TYPE_FREEZE = 1;
 
@@ -70,15 +68,16 @@
     /** Handles call back into clients. */
     private final SingleMessageHandler<CarDiagnosticEvent> mHandlerCallback;
 
-    private CarDiagnosticEventListenerToService mListenerToService;
+    private final CarDiagnosticEventListenerToService mListenerToService;
 
     private final CarPermission mVendorExtensionPermission;
 
     /** @hide */
-    public CarDiagnosticManager(IBinder service, Context context, Handler handler) {
+    public CarDiagnosticManager(Car car, IBinder service) {
+        super(car);
         mService = ICarDiagnostic.Stub.asInterface(service);
-        mHandlerCallback = new SingleMessageHandler<CarDiagnosticEvent>(handler.getLooper(),
-            MSG_DIAGNOSTIC_EVENTS) {
+        mHandlerCallback = new SingleMessageHandler<CarDiagnosticEvent>(
+                getEventHandler().getLooper(), MSG_DIAGNOSTIC_EVENTS) {
             @Override
             protected void handleEvent(CarDiagnosticEvent event) {
                 CarDiagnosticListeners listeners;
@@ -90,7 +89,9 @@
                 }
             }
         };
-        mVendorExtensionPermission = new CarPermission(context, Car.PERMISSION_VENDOR_EXTENSION);
+        mVendorExtensionPermission = new CarPermission(getContext(),
+                Car.PERMISSION_VENDOR_EXTENSION);
+        mListenerToService = new CarDiagnosticEventListenerToService(this);
     }
 
     @Override
@@ -98,7 +99,6 @@
     public void onCarDisconnected() {
         synchronized(mActiveListeners) {
             mActiveListeners.clear();
-            mListenerToService = null;
         }
     }
 
@@ -137,9 +137,6 @@
             OnDiagnosticEventListener listener, @FrameType int frameType, int rate) {
         assertFrameType(frameType);
         synchronized(mActiveListeners) {
-            if (null == mListenerToService) {
-                mListenerToService = new CarDiagnosticEventListenerToService(this);
-            }
             boolean needsServerUpdate = false;
             CarDiagnosticListeners listeners = mActiveListeners.get(frameType);
             if (listeners == null) {
@@ -184,7 +181,8 @@
                     mService.unregisterDiagnosticListener(frameType,
                         mListenerToService);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
+                    // continue for local clean-up
                 }
                 mActiveListeners.remove(frameType);
             } else if (needsServerUpdate) {
@@ -197,7 +195,7 @@
         try {
             return mService.registerOrUpdateDiagnosticListener(frameType, rate, mListenerToService);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -212,7 +210,7 @@
         try {
             return mService.getLatestLiveFrame();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -229,7 +227,7 @@
         try {
             return mService.getFreezeFrameTimestamps();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, new long[0]);
         }
     }
 
@@ -246,7 +244,7 @@
         try {
             return mService.getFreezeFrame(timestamp);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -264,7 +262,7 @@
         try {
             return mService.clearFreezeFrames(timestamps);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -276,7 +274,7 @@
         try {
             return mService.isLiveFrameSupported();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -288,7 +286,7 @@
         try {
             return mService.isFreezeFrameNotificationSupported();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -300,7 +298,7 @@
         try {
             return mService.isGetFreezeFrameSupported();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -318,7 +316,7 @@
         try {
             return mService.isClearFreezeFramesSupported();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -336,7 +334,7 @@
         try {
             return mService.isSelectiveClearFreezeFramesSupported();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
index 9b0626f..4053c5c 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
@@ -22,7 +22,6 @@
 import android.annotation.TestApi;
 import android.car.Car;
 import android.car.CarManagerBase;
-import android.content.Context;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -40,13 +39,12 @@
  */
 @SystemApi
 @TestApi
-public final class CarDrivingStateManager implements CarManagerBase {
+public final class CarDrivingStateManager extends CarManagerBase {
     private static final String TAG = "CarDrivingStateMgr";
     private static final boolean DBG = false;
     private static final boolean VDBG = false;
     private static final int MSG_HANDLE_DRIVING_STATE_CHANGE = 0;
 
-    private final Context mContext;
     private final ICarDrivingState mDrivingService;
     private final EventCallbackHandler mEventCallbackHandler;
     private CarDrivingStateEventListener mDrvStateEventListener;
@@ -54,10 +52,10 @@
 
 
     /** @hide */
-    public CarDrivingStateManager(IBinder service, Context context, Handler handler) {
-        mContext = context;
+    public CarDrivingStateManager(Car car, IBinder service) {
+        super(car);
         mDrivingService = ICarDrivingState.Stub.asInterface(service);
-        mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+        mEventCallbackHandler = new EventCallbackHandler(this, getEventHandler().getLooper());
     }
 
     /** @hide */
@@ -111,7 +109,7 @@
             // register to the Service for getting notified
             mDrivingService.registerDrivingStateChangeListener(mListenerToService);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -134,7 +132,7 @@
             mDrvStateEventListener = null;
             mListenerToService = null;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -151,7 +149,7 @@
         try {
             return mDrivingService.getCurrentDrivingState();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -172,7 +170,7 @@
         try {
             mDrivingService.injectDrivingState(event);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 9a3d5cf..be194b8 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -22,7 +22,6 @@
 import android.annotation.RequiresPermission;
 import android.car.Car;
 import android.car.CarManagerBase;
-import android.content.Context;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -43,7 +42,7 @@
  * API to register and get the User Experience restrictions imposed based on the car's driving
  * state.
  */
-public final class CarUxRestrictionsManager implements CarManagerBase {
+public final class CarUxRestrictionsManager extends CarManagerBase {
     private static final String TAG = "CarUxRManager";
     private static final boolean DBG = false;
     private static final boolean VDBG = false;
@@ -80,7 +79,6 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface UxRestrictionMode {}
 
-    private final Context mContext;
     private int mDisplayId = Display.INVALID_DISPLAY;
     private final ICarUxRestrictionsManager mUxRService;
     private final EventCallbackHandler mEventCallbackHandler;
@@ -89,10 +87,11 @@
     private CarUxRestrictionsChangeListenerToService mListenerToService;
 
     /** @hide */
-    public CarUxRestrictionsManager(IBinder service, Context context, Handler handler) {
-        mContext = context;
+    public CarUxRestrictionsManager(Car car, IBinder service) {
+        super(car);
         mUxRService = ICarUxRestrictionsManager.Stub.asInterface(service);
-        mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+        mEventCallbackHandler = new EventCallbackHandler(this,
+                getEventHandler().getLooper());
     }
 
     /** @hide */
@@ -152,7 +151,7 @@
             // register to the Service to listen for changes.
             mUxRService.registerUxRestrictionsChangeListener(mListenerToService, displayId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -172,7 +171,7 @@
         try {
             mUxRService.unregisterUxRestrictionsChangeListener(mListenerToService);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -197,7 +196,7 @@
         try {
             return mUxRService.saveUxRestrictionsConfigurationForNextBoot(configs);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -219,7 +218,7 @@
         try {
             return mUxRService.getCurrentUxRestrictions(displayId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -233,7 +232,7 @@
         try {
             return mUxRService.setRestrictionMode(mode);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -248,7 +247,7 @@
         try {
             return mUxRService.getRestrictionMode();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -288,7 +287,7 @@
         try {
             return mUxRService.getStagedConfigs();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -304,7 +303,7 @@
         try {
             return mUxRService.getConfigs();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -399,7 +398,7 @@
             return mDisplayId;
         }
 
-        mDisplayId = mContext.getDisplayId();
+        mDisplayId = getContext().getDisplayId();
         Log.i(TAG, "Context returns display ID " + mDisplayId);
 
         if (mDisplayId == Display.INVALID_DISPLAY) {
diff --git a/car-lib/src/android/car/hardware/CarSensorManager.java b/car-lib/src/android/car/hardware/CarSensorManager.java
index d61cb2e..082c8eb 100644
--- a/car-lib/src/android/car/hardware/CarSensorManager.java
+++ b/car-lib/src/android/car/hardware/CarSensorManager.java
@@ -26,9 +26,7 @@
 import android.car.hardware.property.CarPropertyManager;
 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
 import android.car.hardware.property.ICarProperty;
-import android.content.Context;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.util.ArraySet;
 import android.util.Log;
@@ -46,7 +44,7 @@
  *  API for monitoring car sensor data.
  */
 @Deprecated
-public final class CarSensorManager implements CarManagerBase {
+public final class CarSensorManager extends CarManagerBase {
     private static final String TAG = "CarSensorManager";
     private final CarPropertyManager mCarPropertyMgr;
     /** @hide */
@@ -304,9 +302,10 @@
 
     }
     /** @hide */
-    public CarSensorManager(IBinder service, Context context, Handler handler) {
+    public CarSensorManager(Car car, IBinder service) {
+        super(car);
         ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
-        mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+        mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
     }
 
     /** @hide */
diff --git a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
index e7df3b0..b796156 100644
--- a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
+++ b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
@@ -22,7 +22,6 @@
 import android.car.hardware.property.CarPropertyManager;
 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
 import android.car.hardware.property.ICarProperty;
-import android.os.Handler;
 import android.os.IBinder;
 import android.util.ArraySet;
 
@@ -44,7 +43,7 @@
  */
 @Deprecated
 @SystemApi
-public final class CarVendorExtensionManager implements CarManagerBase {
+public final class CarVendorExtensionManager extends CarManagerBase {
 
     private final static boolean DBG = false;
     private final static String TAG = CarVendorExtensionManager.class.getSimpleName();
@@ -84,9 +83,10 @@
      * <p>Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
      * @hide
      */
-    public CarVendorExtensionManager(IBinder service, Handler handler) {
+    public CarVendorExtensionManager(Car car, IBinder service) {
+        super(car);
         ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
-        mPropertyManager = new CarPropertyManager(mCarPropertyService, handler);
+        mPropertyManager = new CarPropertyManager(car, mCarPropertyService);
     }
 
     /**
@@ -206,6 +206,9 @@
     /** @hide */
     @Override
     public void onCarDisconnected() {
+        synchronized (mLock) {
+            mCallbacks.clear();
+        }
         mPropertyManager.onCarDisconnected();
     }
     private static class CarPropertyEventListenerToBase implements CarPropertyEventCallback {
diff --git a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
index 1c41a2b..7318176 100644
--- a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
+++ b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
@@ -25,8 +25,6 @@
 import android.car.hardware.property.CarPropertyManager;
 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
 import android.car.hardware.property.ICarProperty;
-import android.content.Context;
-import android.os.Handler;
 import android.os.IBinder;
 import android.util.ArraySet;
 
@@ -58,7 +56,7 @@
  */
 @Deprecated
 @SystemApi
-public final class CarCabinManager implements CarManagerBase {
+public final class CarCabinManager extends CarManagerBase {
     private final static boolean DBG = false;
     private final static String TAG = "CarCabinManager";
     private final CarPropertyManager mCarPropertyMgr;
@@ -470,9 +468,10 @@
      * @param handler
      * @hide
      */
-    public CarCabinManager(IBinder service, Context context, Handler handler) {
+    public CarCabinManager(Car car, IBinder service) {
+        super(car);
         ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
-        mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+        mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
     }
 
     /**
@@ -594,6 +593,10 @@
     /** @hide */
     @Override
     public void onCarDisconnected() {
+        // TODO(b/142730969) Fix synchronization to use separate mLock
+        synchronized (this) {
+            mCallbacks.clear();
+        }
         mCarPropertyMgr.onCarDisconnected();
     }
 }
diff --git a/car-lib/src/android/car/hardware/hvac/CarHvacManager.java b/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
index b2b8014..3ab7631 100644
--- a/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
+++ b/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
@@ -25,8 +25,6 @@
 import android.car.hardware.property.CarPropertyManager;
 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
 import android.car.hardware.property.ICarProperty;
-import android.content.Context;
-import android.os.Handler;
 import android.os.IBinder;
 import android.util.ArraySet;
 import android.util.Log;
@@ -46,7 +44,7 @@
  */
 @Deprecated
 @SystemApi
-public final class CarHvacManager implements CarManagerBase {
+public final class CarHvacManager extends CarManagerBase {
     private final static boolean DBG = false;
     private final static String TAG = "CarHvacManager";
     private final CarPropertyManager mCarPropertyMgr;
@@ -301,14 +299,15 @@
      *
      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
      * @param service
-     * @param context
-     * @param handler
+     *
      * @hide
      */
-    public CarHvacManager(IBinder service, Context context, Handler handler) {
+    public CarHvacManager(Car car, IBinder service) {
+        super(car);
         ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
-        mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+        mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
     }
+
     /**
      * Implement wrappers for contained CarPropertyManager object
      * @param callback
@@ -432,6 +431,10 @@
 
     /** @hide */
     public void onCarDisconnected() {
+        // TODO(b/142730482) Fix synchronization to use separate mLock
+        synchronized (this) {
+            mCallbacks.clear();
+        }
         mCarPropertyMgr.onCarDisconnected();
     }
 }
diff --git a/car-lib/src/android/car/hardware/power/CarPowerManager.java b/car-lib/src/android/car/hardware/power/CarPowerManager.java
index 3d9a23a..4b0e8cf 100644
--- a/car-lib/src/android/car/hardware/power/CarPowerManager.java
+++ b/car-lib/src/android/car/hardware/power/CarPowerManager.java
@@ -19,8 +19,6 @@
 import android.annotation.SystemApi;
 import android.car.Car;
 import android.car.CarManagerBase;
-import android.content.Context;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -35,15 +33,18 @@
  * @hide
  */
 @SystemApi
-public class CarPowerManager implements CarManagerBase {
+public class CarPowerManager extends CarManagerBase {
     private final static boolean DBG = false;
     private final static String TAG = "CarPowerManager";
 
     private final Object mLock = new Object();
     private final ICarPower mService;
 
+    @GuardedBy("mLock")
     private CarPowerStateListener mListener;
+    @GuardedBy("mLock")
     private CarPowerStateListenerWithCompletion mListenerWithCompletion;
+    @GuardedBy("mLock")
     private CompletableFuture<Void> mFuture;
     @GuardedBy("mLock")
     private ICarPowerStateListener mListenerToService;
@@ -131,7 +132,8 @@
      * @param handler
      * @hide
      */
-    public CarPowerManager(IBinder service, Context context, Handler handler) {
+    public CarPowerManager(Car car, IBinder service) {
+        super(car);
         mService = ICarPower.Stub.asInterface(service);
     }
 
@@ -143,7 +145,7 @@
         try {
             mService.requestShutdownOnNextSuspend();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -155,7 +157,7 @@
         try {
             mService.scheduleNextWakeupTime(seconds);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -209,13 +211,23 @@
                 @Override
                 public void onStateChanged(int state) throws RemoteException {
                     if (useCompletion) {
-                        // Update CompletableFuture. This will recreate it or just clean it up.
-                        updateFuture(state);
+                        CarPowerStateListenerWithCompletion listenerWithCompletion;
+                        CompletableFuture<Void> future;
+                        synchronized (mLock) {
+                            // Update CompletableFuture. This will recreate it or just clean it up.
+                            updateFutureLocked(state);
+                            listenerWithCompletion = mListenerWithCompletion;
+                            future = mFuture;
+                        }
                         // Notify user that the state has changed and supply a future
-                        mListenerWithCompletion.onStateChanged(state, mFuture);
+                        listenerWithCompletion.onStateChanged(state, future);
                     } else {
+                        CarPowerStateListener listener;
+                        synchronized (mLock) {
+                            listener = mListener;
+                        }
                         // Notify the user without supplying a future
-                        mListener.onStateChanged(state);
+                        listener.onStateChanged(state);
                     }
                 }
             };
@@ -227,7 +239,7 @@
                 }
                 mListenerToService = listenerToService;
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                handleRemoteExceptionFromCarService(e);
             }
         }
     }
@@ -243,7 +255,7 @@
             mListenerToService = null;
             mListener = null;
             mListenerWithCompletion = null;
-            cleanupFuture();
+            cleanupFutureLocked();
         }
 
         if (listenerToService == null) {
@@ -254,12 +266,12 @@
         try {
             mService.unregisterListener(listenerToService);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
-    private void updateFuture(int state) {
-        cleanupFuture();
+    private void updateFutureLocked(int state) {
+        cleanupFutureLocked();
         if (state == CarPowerStateListener.SHUTDOWN_PREPARE) {
             // Create a CompletableFuture and pass it to the listener.
             // When the listener completes the future, tell
@@ -269,16 +281,20 @@
                 if (exception != null && !(exception instanceof CancellationException)) {
                     Log.e(TAG, "Exception occurred while waiting for future", exception);
                 }
+                ICarPowerStateListener listenerToService;
+                synchronized (mLock) {
+                    listenerToService = mListenerToService;
+                }
                 try {
-                    mService.finished(mListenerToService);
+                    mService.finished(listenerToService);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             });
         }
     }
 
-    private void cleanupFuture() {
+    private void cleanupFutureLocked() {
         if (mFuture != null) {
             if (!mFuture.isDone()) {
                 mFuture.cancel(false);
@@ -290,13 +306,9 @@
     /** @hide */
     @Override
     public void onCarDisconnected() {
-        ICarPowerStateListener listenerToService;
         synchronized (mLock) {
-            listenerToService = mListenerToService;
-        }
-
-        if (listenerToService != null) {
-            clearListener();
+            mListener = null;
+            mListenerWithCompletion = null;
         }
     }
 }
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyManager.java b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
index 3f7da1d..e3651da 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyManager.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
@@ -21,6 +21,7 @@
 import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.car.hardware.CarPropertyConfig;
 import android.car.hardware.CarPropertyValue;
@@ -44,7 +45,7 @@
  * For details about the individual properties, see the descriptions in
  * hardware/interfaces/automotive/vehicle/types.hal
  */
-public class CarPropertyManager implements CarManagerBase {
+public class CarPropertyManager extends CarManagerBase {
     private static final boolean DBG = false;
     private static final String TAG = "CarPropertyManager";
     private static final int MSG_GENERIC_EVENT = 0;
@@ -93,11 +94,12 @@
      * Get an instance of the CarPropertyManager.
      *
      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
+     * @param car Car instance
      * @param service ICarProperty instance
-     * @param handler The handler to deal with CarPropertyEvent.
      * @hide
      */
-    public CarPropertyManager(@NonNull ICarProperty service, @Nullable Handler handler) {
+    public CarPropertyManager(Car car, @NonNull ICarProperty service) {
+        super(car);
         mService = service;
         try {
             List<CarPropertyConfig> configs = mService.getPropertyList();
@@ -108,11 +110,12 @@
             Log.e(TAG, "getPropertyList exception ", e);
             throw new RuntimeException(e);
         }
-        if (handler == null) {
+        Handler eventHandler = getEventHandler();
+        if (eventHandler == null) {
             mHandler = null;
             return;
         }
-        mHandler = new SingleMessageHandler<CarPropertyEvent>(handler.getLooper(),
+        mHandler = new SingleMessageHandler<CarPropertyEvent>(eventHandler.getLooper(),
             MSG_GENERIC_EVENT) {
             @Override
             protected void handleEvent(CarPropertyEvent event) {
@@ -206,7 +209,7 @@
         try {
             mService.registerListener(propertyId, rate, mCarPropertyEventToService);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
         return true;
     }
@@ -274,7 +277,8 @@
                 try {
                     mService.unregisterListener(propertyId, mCarPropertyEventToService);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
+                    // continue for local clean-up
                 }
                 mActivePropertyListener.remove(propertyId);
             } else if (needsServerUpdate) {
@@ -327,7 +331,7 @@
         try {
             return mService.getReadPermission(propId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, "");
         }
     }
 
@@ -346,7 +350,7 @@
         try {
             return mService.getWritePermission(propId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, "");
         }
     }
 
@@ -363,7 +367,7 @@
             return (propValue != null)
                     && (propValue.getStatus() == CarPropertyValue.STATUS_AVAILABLE);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -449,7 +453,7 @@
             }
             return propVal;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -466,7 +470,7 @@
             CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);
             return propVal;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -488,7 +492,7 @@
         try {
             mService.setProperty(new CarPropertyValue<>(propId, areaId, val));
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
diff --git a/car-lib/src/android/car/input/CarInputHandlingService.java b/car-lib/src/android/car/input/CarInputHandlingService.java
index 518cee1..0ea990f 100644
--- a/car-lib/src/android/car/input/CarInputHandlingService.java
+++ b/car-lib/src/android/car/input/CarInputHandlingService.java
@@ -19,6 +19,7 @@
 import android.annotation.MainThread;
 import android.annotation.SystemApi;
 import android.app.Service;
+import android.car.Car;
 import android.car.CarLibLog;
 import android.content.Intent;
 import android.os.Bundle;
@@ -101,7 +102,7 @@
         try {
             callbackBinder.transact(INPUT_CALLBACK_BINDER_CODE, dataIn, null, IBinder.FLAG_ONEWAY);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            Car.handleRemoteExceptionFromCarService(this, e);
         }
     }
 
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index dcd4514..2bc8fd7 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -22,10 +22,8 @@
 import android.car.Car;
 import android.car.CarLibLog;
 import android.car.CarManagerBase;
-import android.content.Context;
 import android.media.AudioAttributes;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -52,7 +50,7 @@
  * - There is exactly one audio zone, which is the primary zone
  * - Each volume group represents a controllable STREAM_TYPE, same as AudioManager
  */
-public final class CarAudioManager implements CarManagerBase {
+public final class CarAudioManager extends CarManagerBase {
 
     /**
      * Zone id of the primary audio zone.
@@ -114,7 +112,7 @@
         try {
             return mService.isDynamicRoutingEnabled();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -147,7 +145,7 @@
         try {
             mService.setGroupVolume(zoneId, groupId, index, flags);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -177,7 +175,7 @@
         try {
             return mService.getGroupMaxVolume(zoneId, groupId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -207,7 +205,7 @@
         try {
             return mService.getGroupMinVolume(zoneId, groupId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -240,7 +238,7 @@
         try {
             return mService.getGroupVolume(zoneId, groupId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -259,7 +257,7 @@
         try {
             mService.setFadeTowardFront(value);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -278,7 +276,7 @@
         try {
             mService.setBalanceTowardRight(value);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -300,7 +298,8 @@
         try {
             return mService.getExternalSources();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
+            return new String[0];
         }
     }
 
@@ -330,7 +329,7 @@
         try {
             return mService.createAudioPatch(sourceAddress, usage, gainInMillibels);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -350,7 +349,7 @@
         try {
             mService.releaseAudioPatch(patch);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -379,7 +378,7 @@
         try {
             return mService.getVolumeGroupCount(zoneId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -409,7 +408,7 @@
         try {
             return mService.getVolumeGroupIdForUsage(zoneId, usage);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -436,7 +435,7 @@
         try {
             return mService.getAudioZoneIds();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, new int[0]);
         }
     }
 
@@ -453,7 +452,7 @@
         try {
             return mService.getZoneIdForUid(uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -470,7 +469,7 @@
         try {
             return mService.setZoneIdForUid(zoneId, uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -486,7 +485,7 @@
         try {
             return mService.clearZoneIdForUid(uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -523,7 +522,7 @@
         try {
             return mService.getZoneIdForDisplayPortId(displayPortId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -541,7 +540,7 @@
         try {
             return mService.getUsagesForVolumeGroupId(zoneId, groupId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, new int[0]);
         }
     }
 
@@ -552,16 +551,16 @@
             try {
                 mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder());
             } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
+                handleRemoteExceptionFromCarService(e);
             }
         }
     }
 
     /** @hide */
-    public CarAudioManager(IBinder service, Context context, Handler handler) {
+    public CarAudioManager(Car car, IBinder service) {
+        super(car);
         mService = ICarAudio.Stub.asInterface(service);
         mCarVolumeCallbacks = new ArrayList<>();
-
         try {
             mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder());
         } catch (RemoteException e) {
diff --git a/car-lib/src/android/car/media/CarMediaManager.java b/car-lib/src/android/car/media/CarMediaManager.java
index 12c2dc8..8537ed6 100644
--- a/car-lib/src/android/car/media/CarMediaManager.java
+++ b/car-lib/src/android/car/media/CarMediaManager.java
@@ -29,7 +29,7 @@
  * API for updating and receiving updates to the primary media source in the car.
  * @hide
  */
-public final class CarMediaManager implements CarManagerBase {
+public final class CarMediaManager extends CarManagerBase {
 
     private final ICarMedia mService;
     private Map<MediaSourceChangedListener, ICarMediaSourceListener> mCallbackMap = new HashMap();
@@ -40,7 +40,8 @@
      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
      * @hide
      */
-    public CarMediaManager(IBinder service) {
+    public CarMediaManager(Car car, IBinder service) {
+        super(car);
         mService = ICarMedia.Stub.asInterface(service);
     }
 
@@ -67,7 +68,7 @@
         try {
             return mService.getMediaSource();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -81,7 +82,7 @@
         try {
             mService.setMediaSource(componentName);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -102,7 +103,7 @@
             mCallbackMap.put(callback, binderCallback);
             mService.registerMediaSourceListener(binderCallback);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -117,12 +118,14 @@
             ICarMediaSourceListener binderCallback = mCallbackMap.remove(callback);
             mService.unregisterMediaSourceListener(binderCallback);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
     /** @hide */
     @Override
     public synchronized void onCarDisconnected() {
+        // TODO(b/142733057) Fix synchronization to use separate mLock
+        mCallbackMap.clear();
     }
 }
diff --git a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
index a70e3c8..2aa2f10 100644
--- a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
+++ b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
@@ -24,14 +24,13 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Log;
 
 /**
  * API for providing navigation status for instrument cluster.
  * @hide
  */
 @SystemApi
-public final class CarNavigationStatusManager implements CarManagerBase {
+public final class CarNavigationStatusManager extends CarManagerBase {
     private static final String TAG = CarLibLog.TAG_NAV;
 
     private final IInstrumentClusterNavigation mService;
@@ -40,7 +39,8 @@
      * Only for CarServiceLoader
      * @hide
      */
-    public CarNavigationStatusManager(IBinder service) {
+    public CarNavigationStatusManager(Car car, IBinder service) {
+        super(car);
         mService = IInstrumentClusterNavigation.Stub.asInterface(service);
     }
 
@@ -67,14 +67,13 @@
         try {
             mService.onNavigationStateChanged(bundle);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
     /** @hide */
     @Override
     public void onCarDisconnected() {
-        Log.e(TAG, "Car service disconnected");
     }
 
     /** Returns navigation features of instrument cluster */
@@ -83,7 +82,7 @@
         try {
             return mService.getInstrumentClusterInfo();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 }
diff --git a/car-lib/src/android/car/settings/CarConfigurationManager.java b/car-lib/src/android/car/settings/CarConfigurationManager.java
index 34d5f4a..626ad39 100644
--- a/car-lib/src/android/car/settings/CarConfigurationManager.java
+++ b/car-lib/src/android/car/settings/CarConfigurationManager.java
@@ -16,6 +16,7 @@
 
 package android.car.settings;
 
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -23,13 +24,14 @@
 /**
  * Manager that exposes car configuration values that are stored on the system.
  */
-public class CarConfigurationManager implements CarManagerBase {
+public class CarConfigurationManager extends CarManagerBase {
     private static final String TAG = "CarConfigurationManager";
 
     private final ICarConfigurationManager mConfigurationService;
 
     /** @hide */
-    public CarConfigurationManager(IBinder service) {
+    public CarConfigurationManager(Car car, IBinder service) {
+        super(car);
         mConfigurationService = ICarConfigurationManager.Stub.asInterface(service);
     }
 
@@ -42,7 +44,7 @@
         try {
             return mConfigurationService.getSpeedBumpConfiguration();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
diff --git a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
index ff7b099..69c092b 100644
--- a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
+++ b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
@@ -19,7 +19,6 @@
 import android.annotation.SystemApi;
 import android.car.Car;
 import android.car.CarManagerBase;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 
@@ -37,7 +36,7 @@
  * @hide
  */
 @SystemApi
-public final class CarStorageMonitoringManager implements CarManagerBase {
+public final class CarStorageMonitoringManager extends CarManagerBase {
     private static final String TAG = CarStorageMonitoringManager.class.getSimpleName();
     private static final int MSG_IO_STATS_EVENT = 0;
 
@@ -77,9 +76,10 @@
     /**
      * @hide
      */
-    public CarStorageMonitoringManager(IBinder service, Handler handler) {
+    public CarStorageMonitoringManager(Car car, IBinder service) {
+        super(car);
         mService = ICarStorageMonitoring.Stub.asInterface(service);
-        mMessageHandler = new SingleMessageHandler<IoStats>(handler, MSG_IO_STATS_EVENT) {
+        mMessageHandler = new SingleMessageHandler<IoStats>(getEventHandler(), MSG_IO_STATS_EVENT) {
             @Override
             protected void handleEvent(IoStats event) {
                 for (IoStatsListener listener : mListeners) {
@@ -112,7 +112,7 @@
         try {
             return mService.getPreEolIndicatorStatus();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, PRE_EOL_INFO_UNKNOWN);
         }
     }
 
@@ -130,7 +130,7 @@
         try {
             return mService.getWearEstimate();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -150,7 +150,7 @@
         try {
             return mService.getWearEstimateHistory();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
@@ -169,7 +169,7 @@
         try {
             return mService.getBootIoStats();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
@@ -199,7 +199,7 @@
         try {
             return mService.getShutdownDiskWriteAmount();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, 0);
         }
     }
 
@@ -216,7 +216,7 @@
         try {
             return mService.getAggregateIoStats();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
@@ -236,7 +236,7 @@
         try {
             return mService.getIoStatsDeltas();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
@@ -259,7 +259,7 @@
             }
             mListeners.add(listener);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -277,7 +277,7 @@
                 mListenerToService = null;
             }
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 }
diff --git a/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java b/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
index 5e39ea3..0a167cf 100644
--- a/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
+++ b/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
@@ -15,6 +15,7 @@
  */
 package android.car.test;
 
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.os.IBinder;
 
@@ -22,10 +23,17 @@
  * Only for system testing
  * @hide
  */
-public class CarTestManagerBinderWrapper implements CarManagerBase {
+public class CarTestManagerBinderWrapper extends CarManagerBase {
     public final IBinder binder;
 
     public CarTestManagerBinderWrapper(IBinder binder) {
+        super(null); // This will not work safely but is only for keeping API.
+        this.binder = binder;
+    }
+
+    /** @hide */
+    public CarTestManagerBinderWrapper(Car car, IBinder binder) {
+        super(car);
         this.binder = binder;
     }
 
diff --git a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
index c82d515..9881420 100644
--- a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
+++ b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
@@ -24,8 +24,8 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.bluetooth.BluetoothDevice;
+import android.car.Car;
 import android.car.CarManagerBase;
-import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -39,6 +39,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.Collections;
 import java.util.List;
 
 
@@ -67,7 +68,7 @@
  * @hide
  */
 @SystemApi
-public final class CarTrustAgentEnrollmentManager implements CarManagerBase {
+public final class CarTrustAgentEnrollmentManager extends CarManagerBase {
     private static final String TAG = "CarTrustEnrollMgr";
     private static final String KEY_HANDLE = "handle";
     private static final String KEY_ACTIVE = "active";
@@ -81,7 +82,6 @@
     private static final int MSG_ENROLL_TOKEN_STATE_CHANGED = 7;
     private static final int MSG_ENROLL_TOKEN_REMOVED = 8;
 
-    private final Context mContext;
     private final ICarTrustAgentEnrollment mEnrollmentService;
     private Object mListenerLock = new Object();
     @GuardedBy("mListenerLock")
@@ -114,10 +114,10 @@
 
 
     /** @hide */
-    public CarTrustAgentEnrollmentManager(IBinder service, Context context, Handler handler) {
-        mContext = context;
+    public CarTrustAgentEnrollmentManager(Car car, IBinder service) {
+        super(car);
         mEnrollmentService = ICarTrustAgentEnrollment.Stub.asInterface(service);
-        mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+        mEventCallbackHandler = new EventCallbackHandler(this, getEventHandler().getLooper());
     }
 
     /** @hide */
@@ -134,7 +134,7 @@
         try {
             mEnrollmentService.startEnrollmentAdvertising();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -146,7 +146,7 @@
         try {
             mEnrollmentService.stopEnrollmentAdvertising();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -161,7 +161,7 @@
         try {
             mEnrollmentService.enrollmentHandshakeAccepted(device);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -173,7 +173,7 @@
         try {
             mEnrollmentService.terminateEnrollmentHandshake();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -194,7 +194,7 @@
         try {
             return mEnrollmentService.isEscrowTokenActive(handle, uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, false);
         }
     }
 
@@ -209,7 +209,7 @@
         try {
             mEnrollmentService.removeEscrowToken(handle, uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -223,7 +223,7 @@
         try {
             mEnrollmentService.removeAllTrustedDevices(uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -238,7 +238,7 @@
         try {
             mEnrollmentService.setTrustedDeviceEnrollmentEnabled(isEnabled);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -253,7 +253,7 @@
         try {
             mEnrollmentService.setTrustedDeviceUnlockEnabled(isEnabled);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -278,7 +278,7 @@
                     mEnrollmentService.registerEnrollmentCallback(mListenerToEnrollmentService);
                     mEnrollmentCallback = callback;
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             }
         }
@@ -290,7 +290,7 @@
                 try {
                     mEnrollmentService.unregisterEnrollmentCallback(mListenerToEnrollmentService);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
                 mEnrollmentCallback = null;
             }
@@ -318,7 +318,7 @@
                     mEnrollmentService.registerBleCallback(mListenerToBleService);
                     mBleCallback = callback;
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             }
         }
@@ -330,7 +330,7 @@
                 try {
                     mEnrollmentService.unregisterBleCallback(mListenerToBleService);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
                 mBleCallback = null;
             }
@@ -351,7 +351,7 @@
         try {
             return mEnrollmentService.getEnrolledDeviceInfosForUser(uid);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, Collections.emptyList());
         }
     }
 
diff --git a/car-lib/src/android/car/vms/VmsPublisherClientService.java b/car-lib/src/android/car/vms/VmsPublisherClientService.java
index 309d0ee..ea75707 100644
--- a/car-lib/src/android/car/vms/VmsPublisherClientService.java
+++ b/car-lib/src/android/car/vms/VmsPublisherClientService.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.app.Service;
+import android.car.Car;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.Build;
@@ -114,7 +115,7 @@
         try {
             mVmsPublisherService.publish(token, layer, publisherId, payload);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            Car.handleRemoteExceptionFromCarService(this, e);
         }
     }
 
@@ -134,7 +135,7 @@
             mVmsPublisherService.setLayersOffering(token, offering);
             VmsOperationRecorder.get().setLayersOffering(offering);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            Car.handleRemoteExceptionFromCarService(this, e);
         }
     }
 
@@ -172,6 +173,7 @@
             publisherId = mVmsPublisherService.getPublisherId(publisherInfo);
             Log.i(TAG, "Assigned publisher ID: " + publisherId);
         } catch (RemoteException e) {
+            // This will crash. To prevent crash, safer invalid return value should be defined.
             throw e.rethrowFromSystemServer();
         }
         VmsOperationRecorder.get().getPublisherId(publisherId);
@@ -191,7 +193,7 @@
         try {
             return mVmsPublisherService.getSubscriptions();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return Car.handleRemoteExceptionFromCarService(this, e, null);
         }
     }
 
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index c02d3a5..edde982 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -19,6 +19,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.os.Binder;
 import android.os.IBinder;
@@ -39,7 +40,7 @@
  * @hide
  */
 @SystemApi
-public final class VmsSubscriberManager implements CarManagerBase {
+public final class VmsSubscriberManager extends CarManagerBase {
     private static final String TAG = "VmsSubscriberManager";
 
     private final IVmsSubscriberService mVmsSubscriberService;
@@ -75,7 +76,8 @@
      *
      * @hide
      */
-    public VmsSubscriberManager(IBinder service) {
+    public VmsSubscriberManager(Car car, IBinder service) {
+        super(car);
         mVmsSubscriberService = IVmsSubscriberService.Stub.asInterface(service);
         mSubscriberManagerClient = new IVmsSubscriberClient.Stub() {
             @Override
@@ -133,7 +135,7 @@
         try {
             mVmsSubscriberService.addVmsSubscriberToNotifications(mSubscriberManagerClient);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -148,7 +150,7 @@
         try {
             mVmsSubscriberService.removeVmsSubscriberToNotifications(mSubscriberManagerClient);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         } finally {
             synchronized (mClientCallbackLock) {
                 mClientCallback = null;
@@ -168,7 +170,7 @@
         try {
             return mVmsSubscriberService.getPublisherInfo(publisherId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -182,7 +184,7 @@
         try {
             return mVmsSubscriberService.getAvailableLayers();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            return handleRemoteExceptionFromCarService(e, null);
         }
     }
 
@@ -199,7 +201,7 @@
             mVmsSubscriberService.addVmsSubscriber(mSubscriberManagerClient, layer);
             VmsOperationRecorder.get().subscribe(layer);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -218,7 +220,7 @@
                     mSubscriberManagerClient, layer, publisherId);
             VmsOperationRecorder.get().subscribe(layer, publisherId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -231,7 +233,7 @@
             mVmsSubscriberService.addVmsSubscriberPassive(mSubscriberManagerClient);
             VmsOperationRecorder.get().startMonitoring();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -248,7 +250,7 @@
             mVmsSubscriberService.removeVmsSubscriber(mSubscriberManagerClient, layer);
             VmsOperationRecorder.get().unsubscribe(layer);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -266,7 +268,7 @@
                     mSubscriberManagerClient, layer, publisherId);
             VmsOperationRecorder.get().unsubscribe(layer, publisherId);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -278,7 +280,7 @@
             mVmsSubscriberService.removeVmsSubscriberPassive(mSubscriberManagerClient);
             VmsOperationRecorder.get().stopMonitoring();
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -326,5 +328,9 @@
      */
     @Override
     public void onCarDisconnected() {
+        synchronized (mClientCallbackLock) {
+            mClientCallback = null;
+            mExecutor = null;
+        }
     }
 }
diff --git a/car-systemtest-lib/src/android/car/test/CarTestManager.java b/car-systemtest-lib/src/android/car/test/CarTestManager.java
index 52b01a6..3ee1067 100644
--- a/car-systemtest-lib/src/android/car/test/CarTestManager.java
+++ b/car-systemtest-lib/src/android/car/test/CarTestManager.java
@@ -27,18 +27,19 @@
  * @hide
  */
 @SystemApi
-public final class CarTestManager implements CarManagerBase {
+public final class CarTestManager extends CarManagerBase {
 
     private final ICarTest mService;
 
 
-    public CarTestManager(IBinder carServiceBinder) {
+    public CarTestManager(Car car, IBinder carServiceBinder) {
+        super(car);
         mService = ICarTest.Stub.asInterface(carServiceBinder);
     }
 
     @Override
     public void onCarDisconnected() {
-        // should not happen for embedded
+        // test will fail. nothing to do.
     }
 
     /**
@@ -52,7 +53,7 @@
         try {
             mService.stopCarService(token);
         } catch (RemoteException e) {
-            handleRemoteException(e);
+            handleRemoteExceptionFromCarService(e);
         }
     }
 
@@ -66,12 +67,7 @@
         try {
             mService.startCarService(token);
         } catch (RemoteException e) {
-            handleRemoteException(e);
+            handleRemoteExceptionFromCarService(e);
         }
     }
-
-    private static void handleRemoteException(RemoteException e) {
-        // let test fail
-        throw new RuntimeException(e);
-    }
 }
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index 5132890..72feacf 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -73,3 +73,6 @@
 
 $(call inherit-product, $(SRC_TARGET_DIR)/product/core_minimal.mk)
 
+# Default dex optimization configurations
+PRODUCT_PROPERTY_OVERRIDES += \
+     pm.dexopt.disable_bg_dexopt=true
diff --git a/car_product/init/init.bootstat.rc b/car_product/init/init.bootstat.rc
index 5c5e796..4122ea4 100644
--- a/car_product/init/init.bootstat.rc
+++ b/car_product/init/init.bootstat.rc
@@ -4,4 +4,4 @@
 # This is a common source of Android security bugs.
 #
 on property:boot.car_service_created=1
-    exec - root root -- /system/bin/bootstat -r car_service_created
+    exec - system log -- /system/bin/bootstat -r car_service_created
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 6b5ddac..1d5fd14 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -93,4 +93,8 @@
     <string name="config_dataUsageSummaryComponent">com.android.car.settings/com.android.car.settings.datausage.DataWarningAndLimitActivity</string>
 
     <bool name="config_automotiveHideNavBarForKeyboard">true</bool>
+
+    <!-- Turn off Wallpaper service -->
+    <bool name="config_enableWallpaperService">false</bool>
+
 </resources>
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
index d8d8516..7fd5d38 100644
--- a/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
@@ -80,4 +80,41 @@
     <!-- Keep the notification background when the container has been expanded. The children will
          expand inline within the container, so it can keep its original background. -->
     <bool name="config_showGroupNotificationBgWhenExpanded">true</bool>
+
+    <!--
+      Service components below were copied verbatim from frameworks/base/packages/SystemUI/res/values/config.xml,
+      then the services that are not needed by automotive were commented out (to improve boot and user switch time).
+    -->
+    <string-array name="config_systemUIServiceComponents" translatable="false">
+        <item>com.android.systemui.util.NotificationChannels</item>
+        <item>com.android.systemui.statusbar.CommandQueue$CommandQueueStart</item>
+        <item>com.android.systemui.keyguard.KeyguardViewMediator</item>
+<!--
+        <item>com.android.systemui.recents.Recents</item>
+-->
+<!--
+        <item>com.android.systemui.volume.VolumeUI</item>
+-->
+        <item>com.android.systemui.stackdivider.Divider</item>
+        <item>com.android.systemui.SystemBars</item>
+        <item>com.android.systemui.usb.StorageNotification</item>
+        <item>com.android.systemui.power.PowerUI</item>
+        <item>com.android.systemui.media.RingtonePlayer</item>
+        <item>com.android.systemui.keyboard.KeyboardUI</item>
+<!--
+        <item>com.android.systemui.pip.PipUI</item>
+-->
+        <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
+        <item>@string/config_systemUIVendorServiceComponent</item>
+        <item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
+        <item>com.android.systemui.LatencyTester</item>
+        <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
+        <item>com.android.systemui.ScreenDecorations</item>
+        <item>com.android.systemui.biometrics.BiometricDialogImpl</item>
+        <item>com.android.systemui.SliceBroadcastRelayHandler</item>
+        <item>com.android.systemui.SizeCompatModeActivityController</item>
+        <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
+        <item>com.android.systemui.theme.ThemeOverlayController</item>
+    </string-array>
+
 </resources>
diff --git a/car_product/sepolicy/private/bluetooth.te b/car_product/sepolicy/private/bluetooth.te
new file mode 100644
index 0000000..6ba74c2
--- /dev/null
+++ b/car_product/sepolicy/private/bluetooth.te
@@ -0,0 +1 @@
+allow bluetooth mediametrics_service:service_manager find;
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index bc1d74c..6e65a8f 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -12,6 +12,9 @@
 # Allow Car Service to register/access itself with ServiceManager
 add_service(carservice_app, carservice_service)
 
+# Allow Car Service to register its stats service with ServiceManager
+add_service(carservice_app, carstats_service)
+
 allow carservice_app wifi_service:service_manager find;
 
 # Allow Car Service to access certain system services.
diff --git a/car_product/sepolicy/private/service_contexts b/car_product/sepolicy/private/service_contexts
index 7ac544c..38d994c 100644
--- a/car_product/sepolicy/private/service_contexts
+++ b/car_product/sepolicy/private/service_contexts
@@ -1,2 +1,3 @@
 car_service  u:object_r:carservice_service:s0
+car_stats u:object_r:carstats_service:s0
 com.android.car.procfsinspector u:object_r:procfsinspector_service:s0
diff --git a/car_product/sepolicy/private/statsd.te b/car_product/sepolicy/private/statsd.te
new file mode 100644
index 0000000..1a17418
--- /dev/null
+++ b/car_product/sepolicy/private/statsd.te
@@ -0,0 +1,2 @@
+# Allow statsd to pull atoms from car_stats service
+allow statsd carstats_service:service_manager find;
diff --git a/car_product/sepolicy/public/property_contexts b/car_product/sepolicy/public/property_contexts
index 8dbe0bc..9646ac9 100644
--- a/car_product/sepolicy/public/property_contexts
+++ b/car_product/sepolicy/public/property_contexts
@@ -1 +1,3 @@
+android.car.number_pre_created_guests            u:object_r:car_bootuser_prop:s0
+android.car.number_pre_created_users             u:object_r:car_bootuser_prop:s0
 android.car.systemuser.bootuseroverrideid        u:object_r:car_bootuser_prop:s0
diff --git a/car_product/sepolicy/public/service.te b/car_product/sepolicy/public/service.te
index 87426f4..c6a2e30 100644
--- a/car_product/sepolicy/public/service.te
+++ b/car_product/sepolicy/public/service.te
@@ -1,2 +1,3 @@
 type carservice_service, app_api_service, service_manager_type;
+type carstats_service, service_manager_type;
 type procfsinspector_service, service_manager_type;
diff --git a/car_product/sepolicy/public/te_macros b/car_product/sepolicy/public/te_macros
new file mode 100644
index 0000000..963afdc
--- /dev/null
+++ b/car_product/sepolicy/public/te_macros
@@ -0,0 +1,7 @@
+# Define a macro to allow extra HAL dump
+define(`dump_extra_hal', `
+  hal_client_domain(dumpstate, $1);
+  allow $1_server dumpstate:fifo_file write;
+  allow $1_server dumpstate:fd use;
+  allow dumpstate $1:process signal;
+')
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index ebea889..e2db410 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -475,6 +475,16 @@
         android:label="@string/car_permission_label_enroll_trust"
         android:description="@string/car_permission_desc_enroll_trust" />
 
+    <!-- Allows a test application to control car service's testing mode.
+         This is only for platform level testing.
+         <p>Protection level: signature|privileged
+    -->
+    <permission
+        android:name="android.car.permission.CAR_TEST_SERVICE"
+        android:protectionLevel="signature|privileged"
+        android:label="@string/car_permission_label_car_test_service"
+        android:description="@string/car_permission_desc_car_test_service" />
+
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.DEVICE_POWER" />
     <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 3eb1007..c756510 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -20,10 +20,6 @@
 <!-- Resources to configure car service based on each OEM's preference. -->
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Configuration to enable media center to autoplay when the media source is changed.
-         If this is set to true, media will play automatically when a media source is selected and
-         the selected media source supports media playback. -->
-    <bool name="autoPlayOnMediaSourceChanged">false</bool>
 
     <!--  Configuration to enable usage of dynamic audio routing. If this is set to false,
           dynamic audio routing is disabled and audio works in legacy mode. It may be useful
@@ -236,4 +232,17 @@
          resolve permission by itself to use any higher priority window type.
          Setting this string to empty will disable the feature. -->
     <string name="config_userNoticeUiService" translatable="false">com.google.android.car.kitchensink/.UserNoiticeDemoUiService</string>
+
+    <!-- Configuration to enable media center to autoplay when the media source is changed.
+         There are 3 supported configurations:
+         0 - never play on change
+         1 - always play
+         2 - adaptive, play based on last remembered playback state -->
+    <integer name="config_mediaSourceChangedAutoplay">2</integer>
+    <!-- Configuration to enable media center to autoplay on boot -->
+    <integer name="config_mediaBootAutoplay">2</integer>
+
+    <!-- Disable switching the user while the system is resuming from Suspend to RAM.
+         This default says to prevent changing the user during Resume. -->
+    <bool name="config_disableUserSwitchDuringResume" translatable="false">true</bool>
 </resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index ef731b8..1589180 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -256,6 +256,11 @@
     <string name="car_permission_label_enroll_trust">Enroll Trusted Device</string>
     <string name="car_permission_desc_enroll_trust">Allow Trusted Device Enrollment</string>
 
+    <!-- Permission text: Control car's test mode [CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_car_test_service">Control car\u2019s test mode</string>
+    <!-- Permission text: Control car's test mode [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_car_test_service">Control car\u2019s test mode</string>
+
     <!-- The default name of device enrolled as trust device [CHAR LIMIT=NONE] -->
     <string name="trust_device_default_name">My Device</string>
 
diff --git a/service/src/com/android/car/BinderInterfaceContainer.java b/service/src/com/android/car/BinderInterfaceContainer.java
index a03b633..5d57b16 100644
--- a/service/src/com/android/car/BinderInterfaceContainer.java
+++ b/service/src/com/android/car/BinderInterfaceContainer.java
@@ -122,8 +122,10 @@
     public synchronized void clear() {
         Collection<BinderInterface<T>> interfaces = getInterfaces();
         for (BinderInterface<T> bInterface : interfaces) {
-            removeBinder(bInterface.binderInterface);
+            IBinder binder = bInterface.binderInterface.asBinder();
+            binder.unlinkToDeath(bInterface, 0);
         }
+        mBinders.clear();
     }
 
     private void handleBinderDeath(BinderInterface<T> bInterface) {
diff --git a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
index 4bb2790..c0f393a 100644
--- a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
+++ b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
@@ -117,7 +117,7 @@
             }
         }
     }
-    private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
+    private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
 
     /**
      * Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
@@ -153,6 +153,7 @@
         mUserId = userId;
         mContext = Objects.requireNonNull(context);
         mCarBluetoothService = bluetoothService;
+        mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
         mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
     }
 
@@ -160,9 +161,8 @@
      * Setup the Bluetooth profile service connections and Vehicle Event listeners.
      * and start the state machine -{@link BluetoothAutoConnectStateMachine}
      */
-    public synchronized void init() {
+    public void init() {
         logd("init()");
-        mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
         IntentFilter profileFilter = new IntentFilter();
         profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
         mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
@@ -190,7 +190,7 @@
      * Clean up slate. Close the Bluetooth profile service connections and quit the state machine -
      * {@link BluetoothAutoConnectStateMachine}
      */
-    public synchronized void release() {
+    public void release() {
         logd("release()");
         if (mCarPowerManager != null) {
             mCarPowerManager.clearListener();
@@ -198,7 +198,6 @@
         }
         if (mBluetoothBroadcastReceiver != null) {
             mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
-            mBluetoothBroadcastReceiver = null;
         }
     }
 
@@ -250,7 +249,7 @@
     /**
      * Print the verbose status of the object
      */
-    public synchronized void dump(PrintWriter writer, String indent) {
+    public void dump(PrintWriter writer, String indent) {
         writer.println(indent + TAG + ":");
         writer.println(indent + "\tUserId: " + mUserId);
     }
diff --git a/service/src/com/android/car/BluetoothProfileDeviceManager.java b/service/src/com/android/car/BluetoothProfileDeviceManager.java
index 505a05b..e73d9ea 100644
--- a/service/src/com/android/car/BluetoothProfileDeviceManager.java
+++ b/service/src/com/android/car/BluetoothProfileDeviceManager.java
@@ -46,6 +46,8 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -114,23 +116,30 @@
                         }, new int[] {}));
     }
 
+    // Fixed per-profile information for the profile this object manages
     private final int mProfileId;
     private final String mSettingsKey;
     private final String mProfileConnectionAction;
     private final ParcelUuid[] mProfileUuids;
     private final int[] mProfileTriggers;
+
+    // Central priority list of devices
+    private final Object mPrioritizedDevicesLock = new Object();
+    @GuardedBy("mPrioritizedDevicesLock")
     private ArrayList<BluetoothDevice> mPrioritizedDevices;
 
-    private BluetoothAdapter mBluetoothAdapter;
-    private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
-
-    private ICarBluetoothUserService mBluetoothUserProxies;
-
+    // Auto connection process state
     private final Object mAutoConnectLock = new Object();
+    @GuardedBy("mAutoConnectLock")
     private boolean mConnecting = false;
+    @GuardedBy("mAutoConnectLock")
     private int mAutoConnectPriority;
+    @GuardedBy("mAutoConnectLock")
     private ArrayList<BluetoothDevice> mAutoConnectingDevices;
 
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
+    private final ICarBluetoothUserService mBluetoothUserProxies;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
     /**
@@ -313,6 +322,7 @@
         mProfileUuids = bpi.mUuids;
         mProfileTriggers = bpi.mProfileTriggers;
 
+        mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
         mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
     }
 
@@ -327,7 +337,7 @@
             mAutoConnectPriority = -1;
             mAutoConnectingDevices = null;
         }
-        mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
+
         IntentFilter profileFilter = new IntentFilter();
         profileFilter.addAction(mProfileConnectionAction);
         profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
@@ -347,7 +357,6 @@
             if (mContext != null) {
                 mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
             }
-            mBluetoothBroadcastReceiver = null;
         }
         cancelAutoConnecting();
         commit();
@@ -392,7 +401,7 @@
             }
         }
 
-        synchronized (this) {
+        synchronized (mPrioritizedDevicesLock) {
             mPrioritizedDevices = devices;
         }
 
@@ -408,7 +417,7 @@
     private boolean commit() {
         StringBuilder sb = new StringBuilder();
         String delimiter = "";
-        synchronized (this) {
+        synchronized (mPrioritizedDevicesLock) {
             for (BluetoothDevice device : mPrioritizedDevices) {
                 sb.append(delimiter);
                 sb.append(device.getAddress());
@@ -434,7 +443,7 @@
             addDevice(device); // No-op if device is already in the priority list
         }
 
-        synchronized (this) {
+        synchronized (mPrioritizedDevicesLock) {
             ArrayList<BluetoothDevice> devices = getDeviceListSnapshot();
             for (BluetoothDevice device : devices) {
                 if (!bondedDevices.contains(device)) {
@@ -451,7 +460,7 @@
      */
     public ArrayList<BluetoothDevice> getDeviceListSnapshot() {
         ArrayList<BluetoothDevice> devices = new ArrayList<>();
-        synchronized (this) {
+        synchronized (mPrioritizedDevicesLock) {
             devices = (ArrayList) mPrioritizedDevices.clone();
         }
         return devices;
@@ -462,12 +471,14 @@
      *
      * @param device - The device you wish to add
      */
-    public synchronized void addDevice(BluetoothDevice device) {
+    public void addDevice(BluetoothDevice device) {
         if (device == null) return;
-        if (mPrioritizedDevices.contains(device)) return;
-        logd("Add device " + device);
-        mPrioritizedDevices.add(device);
-        commit();
+        synchronized (mPrioritizedDevicesLock) {
+            if (mPrioritizedDevices.contains(device)) return;
+            logd("Add device " + device);
+            mPrioritizedDevices.add(device);
+            commit();
+        }
     }
 
     /**
@@ -475,12 +486,14 @@
      *
      * @param device - The device you wish to remove
      */
-    public synchronized void removeDevice(BluetoothDevice device) {
+    public void removeDevice(BluetoothDevice device) {
         if (device == null) return;
-        if (!mPrioritizedDevices.contains(device)) return;
-        logd("Remove device " + device);
-        mPrioritizedDevices.remove(device);
-        commit();
+        synchronized (mPrioritizedDevicesLock) {
+            if (!mPrioritizedDevices.contains(device)) return;
+            logd("Remove device " + device);
+            mPrioritizedDevices.remove(device);
+            commit();
+        }
     }
 
     /**
@@ -489,10 +502,12 @@
      * @param device - The device you want the priority of
      * @return The priority of the device, or -1 if the device is not in the list
      */
-    public synchronized int getDeviceConnectionPriority(BluetoothDevice device) {
+    public int getDeviceConnectionPriority(BluetoothDevice device) {
         if (device == null) return -1;
         logd("Get connection priority of " + device);
-        return mPrioritizedDevices.indexOf(device);
+        synchronized (mPrioritizedDevicesLock) {
+            return mPrioritizedDevices.indexOf(device);
+        }
     }
 
     /**
@@ -505,16 +520,18 @@
      * @param device - The device you want to set the priority of
      * @param priority - The priority you want to the device to have
      */
-    public synchronized void setDeviceConnectionPriority(BluetoothDevice device, int priority) {
-        if (device == null || priority < 0 || priority > mPrioritizedDevices.size()
-                || getDeviceConnectionPriority(device) == priority) return;
-        if (mPrioritizedDevices.contains(device)) {
-            mPrioritizedDevices.remove(device);
-            if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size();
+    public void setDeviceConnectionPriority(BluetoothDevice device, int priority) {
+        synchronized (mPrioritizedDevicesLock) {
+            if (device == null || priority < 0 || priority > mPrioritizedDevices.size()
+                    || getDeviceConnectionPriority(device) == priority) return;
+            if (mPrioritizedDevices.contains(device)) {
+                mPrioritizedDevices.remove(device);
+                if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size();
+            }
+            logd("Set connection priority of " + device + " to " + priority);
+            mPrioritizedDevices.add(priority, device);
+            commit();
         }
-        logd("Set connection priority of " + device + " to " + priority);
-        mPrioritizedDevices.add(priority, device);
-        commit();
     }
 
     /**
diff --git a/service/src/com/android/car/BluetoothProfileInhibitManager.java b/service/src/com/android/car/BluetoothProfileInhibitManager.java
index 4dcc6b7..691592f 100644
--- a/service/src/com/android/car/BluetoothProfileInhibitManager.java
+++ b/service/src/com/android/car/BluetoothProfileInhibitManager.java
@@ -56,14 +56,16 @@
     private final int mUserId;
     private final ICarBluetoothUserService mBluetoothUserProxies;
 
-    @GuardedBy("this")
+    private final Object mProfileInhibitsLock = new Object();
+
+    @GuardedBy("mProfileInhibitsLock")
     private final SetMultimap<BluetoothConnection, InhibitRecord> mProfileInhibits =
             new SetMultimap<>();
 
-    @GuardedBy("this")
+    @GuardedBy("mProfileInhibitsLock")
     private final HashSet<InhibitRecord> mRestoredInhibits = new HashSet<>();
 
-    @GuardedBy("this")
+    @GuardedBy("mProfileInhibitsLock")
     private final HashSet<BluetoothConnection> mAlreadyDisabledProfiles = new HashSet<>();
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -190,7 +192,7 @@
         }
 
         public boolean removeSelf() {
-            synchronized (BluetoothProfileInhibitManager.this) {
+            synchronized (mProfileInhibitsLock) {
                 if (mRemoved) {
                     return true;
                 }
@@ -328,9 +330,7 @@
 
         BluetoothConnection params = new BluetoothConnection(profile, device);
         InhibitRecord record;
-        synchronized (this) {
-            record = findInhibitRecord(params, token);
-        }
+        record = findInhibitRecord(params, token);
 
         if (record == null) {
             Log.e(TAG, "Record not found");
@@ -343,64 +343,66 @@
     /**
      * Add a profile inhibit record, disabling the profile if necessary.
      */
-    private synchronized boolean addInhibitRecord(InhibitRecord record) {
-        BluetoothConnection params = record.getParams();
-        if (!isProxyAvailable(params.getProfile())) {
-            return false;
-        }
-
-        Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
-        if (findInhibitRecord(params, record.getToken()) != null) {
-            Log.e(TAG, "Inhibit request already registered - skipping duplicate");
-            return false;
-        }
-
-        try {
-            record.getToken().linkToDeath(record, 0);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Could not link to death on inhibit token (already dead?)", e);
-            return false;
-        }
-
-        boolean isNewlyAdded = previousRecords.isEmpty();
-        mProfileInhibits.put(params, record);
-
-        if (isNewlyAdded) {
-            try {
-                int priority =
-                        mBluetoothUserProxies.getProfilePriority(
-                                params.getProfile(),
-                                params.getDevice());
-                if (priority == BluetoothProfile.PRIORITY_OFF) {
-                    // This profile was already disabled (and not as the result of an inhibit).
-                    // Add it to the already-disabled list, and do nothing else.
-                    mAlreadyDisabledProfiles.add(params);
-
-                    logd("Profile " + Utils.getProfileName(params.getProfile())
-                            + " already disabled for device " + params.getDevice()
-                            + " - suppressing re-enable");
-                } else {
-                    mBluetoothUserProxies.setProfilePriority(
-                            params.getProfile(),
-                            params.getDevice(),
-                            BluetoothProfile.PRIORITY_OFF);
-                    mBluetoothUserProxies.bluetoothDisconnectFromProfile(
-                            params.getProfile(),
-                            params.getDevice());
-                    logd("Disabled profile "
-                            + Utils.getProfileName(params.getProfile())
-                            + " for device " + params.getDevice());
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Could not disable profile", e);
-                record.getToken().unlinkToDeath(record, 0);
-                mProfileInhibits.remove(params, record);
+    private boolean addInhibitRecord(InhibitRecord record) {
+        synchronized (mProfileInhibitsLock) {
+            BluetoothConnection params = record.getParams();
+            if (!isProxyAvailable(params.getProfile())) {
                 return false;
             }
-        }
 
-        commit();
-        return true;
+            Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
+            if (findInhibitRecord(params, record.getToken()) != null) {
+                Log.e(TAG, "Inhibit request already registered - skipping duplicate");
+                return false;
+            }
+
+            try {
+                record.getToken().linkToDeath(record, 0);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not link to death on inhibit token (already dead?)", e);
+                return false;
+            }
+
+            boolean isNewlyAdded = previousRecords.isEmpty();
+            mProfileInhibits.put(params, record);
+
+            if (isNewlyAdded) {
+                try {
+                    int priority =
+                            mBluetoothUserProxies.getProfilePriority(
+                                    params.getProfile(),
+                                    params.getDevice());
+                    if (priority == BluetoothProfile.PRIORITY_OFF) {
+                        // This profile was already disabled (and not as the result of an inhibit).
+                        // Add it to the already-disabled list, and do nothing else.
+                        mAlreadyDisabledProfiles.add(params);
+
+                        logd("Profile " + Utils.getProfileName(params.getProfile())
+                                + " already disabled for device " + params.getDevice()
+                                + " - suppressing re-enable");
+                    } else {
+                        mBluetoothUserProxies.setProfilePriority(
+                                params.getProfile(),
+                                params.getDevice(),
+                                BluetoothProfile.PRIORITY_OFF);
+                        mBluetoothUserProxies.bluetoothDisconnectFromProfile(
+                                params.getProfile(),
+                                params.getDevice());
+                        logd("Disabled profile "
+                                + Utils.getProfileName(params.getProfile())
+                                + " for device " + params.getDevice());
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not disable profile", e);
+                    record.getToken().unlinkToDeath(record, 0);
+                    mProfileInhibits.remove(params, record);
+                    return false;
+                }
+            }
+
+            commit();
+            return true;
+        }
     }
 
     /**
@@ -411,41 +413,45 @@
      * @return InhibitRecord for the connection parameters and token if exists, null otherwise.
      */
     private InhibitRecord findInhibitRecord(BluetoothConnection params, IBinder token) {
-        return mProfileInhibits.get(params)
-            .stream()
-            .filter(r -> r.getToken() == token)
-            .findAny()
-            .orElse(null);
+        synchronized (mProfileInhibitsLock) {
+            return mProfileInhibits.get(params)
+                .stream()
+                .filter(r -> r.getToken() == token)
+                .findAny()
+                .orElse(null);
+        }
     }
 
     /**
      * Remove a given profile inhibit record, reconnecting if necessary.
      */
-    private synchronized boolean removeInhibitRecord(InhibitRecord record) {
-        BluetoothConnection params = record.getParams();
-        if (!isProxyAvailable(params.getProfile())) {
-            return false;
-        }
-        if (!mProfileInhibits.containsEntry(params, record)) {
-            Log.e(TAG, "Record already removed");
-            // Removing something a second time vacuously succeeds.
-            return true;
-        }
-
-        // Re-enable profile before unlinking and removing the record, in case of error.
-        // The profile should be re-enabled if this record is the only one left for that
-        // device and profile combination.
-        if (mProfileInhibits.get(params).size() == 1) {
-            if (!restoreProfilePriority(params)) {
+    private boolean removeInhibitRecord(InhibitRecord record) {
+        synchronized (mProfileInhibitsLock) {
+            BluetoothConnection params = record.getParams();
+            if (!isProxyAvailable(params.getProfile())) {
                 return false;
             }
+            if (!mProfileInhibits.containsEntry(params, record)) {
+                Log.e(TAG, "Record already removed");
+                // Removing something a second time vacuously succeeds.
+                return true;
+            }
+
+            // Re-enable profile before unlinking and removing the record, in case of error.
+            // The profile should be re-enabled if this record is the only one left for that
+            // device and profile combination.
+            if (mProfileInhibits.get(params).size() == 1) {
+                if (!restoreProfilePriority(params)) {
+                    return false;
+                }
+            }
+
+            record.getToken().unlinkToDeath(record, 0);
+            mProfileInhibits.remove(params, record);
+
+            commit();
+            return true;
         }
-
-        record.getToken().unlinkToDeath(record, 0);
-        mProfileInhibits.remove(params, record);
-
-        commit();
-        return true;
     }
 
     /**
@@ -504,46 +510,51 @@
      * Keep trying to remove all profile inhibits that were restored from settings
      * until all such inhibits have been removed.
      */
-    private synchronized void removeRestoredProfileInhibits() {
-        tryRemoveRestoredProfileInhibits();
+    private void removeRestoredProfileInhibits() {
+        synchronized (mProfileInhibitsLock) {
+            tryRemoveRestoredProfileInhibits();
 
-        if (!mRestoredInhibits.isEmpty()) {
-            logd("Could not remove all restored profile inhibits - "
-                        + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
-            mHandler.postDelayed(
-                    this::removeRestoredProfileInhibits,
-                    RESTORED_PROFILE_INHIBIT_TOKEN,
-                    RESTORE_BACKOFF_MILLIS);
+            if (!mRestoredInhibits.isEmpty()) {
+                logd("Could not remove all restored profile inhibits - "
+                            + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
+                mHandler.postDelayed(
+                        this::removeRestoredProfileInhibits,
+                        RESTORED_PROFILE_INHIBIT_TOKEN,
+                        RESTORE_BACKOFF_MILLIS);
+            }
         }
     }
 
     /**
      * Release all active inhibit records prior to user switch or shutdown
      */
-    private synchronized void releaseAllInhibitsBeforeUnbind() {
+    private  void releaseAllInhibitsBeforeUnbind() {
         logd("Unbinding CarBluetoothUserService - releasing all profile inhibits");
-        for (BluetoothConnection params : mProfileInhibits.keySet()) {
-            for (InhibitRecord record : mProfileInhibits.get(params)) {
-                record.removeSelf();
+
+        synchronized (mProfileInhibitsLock) {
+            for (BluetoothConnection params : mProfileInhibits.keySet()) {
+                for (InhibitRecord record : mProfileInhibits.get(params)) {
+                    record.removeSelf();
+                }
             }
+
+            // Some inhibits might be hanging around because they couldn't be cleaned up.
+            // Make sure they get persisted...
+            commit();
+
+            // ...then clear them from the map.
+            mProfileInhibits.clear();
+
+            // We don't need to maintain previously-disabled profiles any more - they were already
+            // skipped in saveProfileInhibitsToSettings() above, and they don't need any
+            // further handling when the user resumes.
+            mAlreadyDisabledProfiles.clear();
+
+            // Clean up bookkeeping for restored inhibits. (If any are still around, they'll be
+            // restored again when this user restarts.)
+            mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN);
+            mRestoredInhibits.clear();
         }
-
-        // Some inhibits might be hanging around because they couldn't be cleaned up.
-        // Make sure they get persisted...
-        commit();
-
-        // ...then clear them from the map.
-        mProfileInhibits.clear();
-
-        // We don't need to maintain previously-disabled profiles any more - they were already
-        // skipped in saveProfileInhibitsToSettings() above, and they don't need any
-        // further handling when the user resumes.
-        mAlreadyDisabledProfiles.clear();
-
-        // Clean up bookkeeping for restored inhibits. (If any are still around, they'll be
-        // restored again when this user restarts.)
-        mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN);
-        mRestoredInhibits.clear();
     }
 
     /**
@@ -564,7 +575,7 @@
     /**
      * Print the verbose status of the object
      */
-    public synchronized void dump(PrintWriter writer, String indent) {
+    public void dump(PrintWriter writer, String indent) {
         writer.println(indent + TAG + ":");
 
         // User metadata
@@ -572,7 +583,7 @@
 
         // Current inhibits
         String inhibits;
-        synchronized (this) {
+        synchronized (mProfileInhibitsLock) {
             inhibits = mProfileInhibits.keySet().toString();
         }
         writer.println(indent + "\tInhibited profiles: " + inhibits);
diff --git a/service/src/com/android/car/CarBluetoothService.java b/service/src/com/android/car/CarBluetoothService.java
index 0c992e1..54d15a1 100644
--- a/service/src/com/android/car/CarBluetoothService.java
+++ b/service/src/com/android/car/CarBluetoothService.java
@@ -28,6 +28,8 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -63,20 +65,31 @@
             BluetoothProfile.PAN
     );
 
+    // Each time PerUserCarService connects we need to get new Bluetooth profile proxies and refresh
+    // all our internal objects to use them. When it disconnects we're to assume our proxies are
+    // invalid. This lock protects all our internal objects.
+    private final Object mPerUserLock = new Object();
+
     // Set of Bluetooth Profile Device Managers, own the priority connection lists, updated on user
     // switch
-    private SparseArray<BluetoothProfileDeviceManager> mProfileDeviceManagers = new SparseArray<>();
+    private final SparseArray<BluetoothProfileDeviceManager> mProfileDeviceManagers =
+            new SparseArray<>();
 
     // Profile-Inhibit Manager that will temporarily inhibit connections on profiles, per user
+    @GuardedBy("mPerUserLock")
     private BluetoothProfileInhibitManager mInhibitManager = null;
 
     // Default Bluetooth device connection policy, per user, enabled with an overlay
     private final boolean mUseDefaultPolicy;
+    @GuardedBy("mPerUserLock")
     private BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicy = null;
 
     // Listen for user switch events from the PerUserCarService
+    @GuardedBy("mPerUserLock")
     private int mUserId;
+    @GuardedBy("mPerUserLock")
     private ICarUserService mCarUserService;
+    @GuardedBy("mPerUserLock")
     private ICarBluetoothUserService mCarBluetoothUserService;
     private final PerUserCarServiceHelper mUserServiceHelper;
     private final PerUserCarServiceHelper.ServiceCallback mUserServiceCallback =
@@ -84,8 +97,14 @@
         @Override
         public void onServiceConnected(ICarUserService carUserService) {
             logd("Connected to PerUserCarService");
-            synchronized (this) {
+            synchronized (mPerUserLock) {
+                // Explicitly clear out existing per-user objects since we can't rely on the
+                // onServiceDisconnected and onPreUnbind calls to always be called before this
+                destroyUser();
+
                 mCarUserService = carUserService;
+
+                // Create new objects with our new set of profile proxies
                 initializeUser();
             }
         }
@@ -99,9 +118,7 @@
         @Override
         public void onServiceDisconnected() {
             logd("Disconnected from PerUserCarService");
-            synchronized (this) {
-                mCarUserService = null;
-            }
+            destroyUser();
         }
     };
 
@@ -126,7 +143,7 @@
      * Wait for the user service helper to report a user before initializing a user.
      */
     @Override
-    public synchronized void init() {
+    public void init() {
         logd("init()");
         mUserServiceHelper.registerServiceCallback(mUserServiceCallback);
     }
@@ -137,11 +154,10 @@
      * Clean up the user context once we've detached from the user service helper, if any.
      */
     @Override
-    public synchronized void release() {
+    public void release() {
         logd("release()");
         mUserServiceHelper.unregisterServiceCallback(mUserServiceCallback);
         destroyUser();
-        mCarUserService = null;
     }
 
     /**
@@ -158,32 +174,37 @@
      *
      * Only call this following a known user switch once we've connected to the user service helper.
      */
-    private synchronized void initializeUser() {
+    private void initializeUser() {
         logd("Initializing new user");
-        mUserId = ActivityManager.getCurrentUser();
-        createBluetoothUserService();
-        createBluetoothProfileDeviceManagers();
-        createBluetoothProfileInhibitManager();
+        synchronized (mPerUserLock) {
+            mUserId = ActivityManager.getCurrentUser();
+            createBluetoothUserService();
+            createBluetoothProfileDeviceManagers();
+            createBluetoothProfileInhibitManager();
 
-        // Determine if we need to begin the default policy
-        mBluetoothDeviceConnectionPolicy = null;
-        if (mUseDefaultPolicy) {
-            createBluetoothDeviceConnectionPolicy();
+            // Determine if we need to begin the default policy
+            mBluetoothDeviceConnectionPolicy = null;
+            if (mUseDefaultPolicy) {
+                createBluetoothDeviceConnectionPolicy();
+            }
+            logd("Switched to user " + mUserId);
         }
-        logd("Switched to user " + mUserId);
     }
 
     /**
      * Destroy the current user context, defined by the set of profile proxies, profile device
      * managers, inhibit manager and the policy.
      */
-    private synchronized void destroyUser() {
+    private void destroyUser() {
         logd("Destroying user " + mUserId);
-        destroyBluetoothDeviceConnectionPolicy();
-        destroyBluetoothProfileInhibitManager();
-        destroyBluetoothProfileDeviceManagers();
-        destroyBluetoothUserService();
-        mUserId = -1;
+        synchronized (mPerUserLock) {
+            destroyBluetoothDeviceConnectionPolicy();
+            destroyBluetoothProfileInhibitManager();
+            destroyBluetoothProfileDeviceManagers();
+            destroyBluetoothUserService();
+            mCarUserService = null;
+            mUserId = -1;
+        }
     }
 
     /**
@@ -192,17 +213,17 @@
      * Also sets up the connection proxy objects required to communicate with the Bluetooth
      * Profile Services.
      */
-    private synchronized void createBluetoothUserService() {
-        if (mCarUserService != null) {
-            try {
-                mCarBluetoothUserService = mCarUserService.getBluetoothUserService();
-                mCarBluetoothUserService.setupBluetoothConnectionProxies();
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
-                        + e.getMessage());
+    private void createBluetoothUserService() {
+        synchronized (mPerUserLock) {
+            if (mCarUserService != null) {
+                try {
+                    mCarBluetoothUserService = mCarUserService.getBluetoothUserService();
+                    mCarBluetoothUserService.setupBluetoothConnectionProxies();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
+                            + e.getMessage());
+                }
             }
-        } else {
-            logd("PerUserCarService not connected. Cannot get bluetooth user proxy objects");
         }
     }
 
@@ -210,109 +231,130 @@
      * Close out the Per User Car Bluetooth profile proxy connections and destroys the Car Bluetooth
      * User Service object.
      */
-    private synchronized void destroyBluetoothUserService() {
-        if (mCarBluetoothUserService == null) return;
-        try {
-            mCarBluetoothUserService.closeBluetoothConnectionProxies();
-        } catch (RemoteException e) {
-            Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
-                    + e.getMessage());
+    private void destroyBluetoothUserService() {
+        synchronized (mPerUserLock) {
+            if (mCarBluetoothUserService == null) return;
+            try {
+                mCarBluetoothUserService.closeBluetoothConnectionProxies();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
+                        + e.getMessage());
+            }
+            mCarBluetoothUserService = null;
         }
-        mCarBluetoothUserService = null;
     }
 
     /**
      * Clears out Profile Device Managers and re-creates them for the current user.
      */
-    private synchronized void createBluetoothProfileDeviceManagers() {
-        mProfileDeviceManagers.clear();
-        if (mUserId == -1) {
-            logd("No foreground user, cannot create profile device managers");
-            return;
-        }
-        for (int profileId : sManagedProfiles) {
-            BluetoothProfileDeviceManager deviceManager = BluetoothProfileDeviceManager.create(
-                    mContext, mUserId, mCarBluetoothUserService, profileId);
-            if (deviceManager == null) {
-                logd("Failed to create profile device manager for "
-                        + Utils.getProfileName(profileId));
-                continue;
+    private void createBluetoothProfileDeviceManagers() {
+        synchronized (mPerUserLock) {
+            if (mUserId == -1) {
+                logd("No foreground user, cannot create profile device managers");
+                return;
             }
-            mProfileDeviceManagers.put(profileId, deviceManager);
-            logd("Created profile device manager for " + Utils.getProfileName(profileId));
-        }
+            for (int profileId : sManagedProfiles) {
+                BluetoothProfileDeviceManager deviceManager = mProfileDeviceManagers.get(profileId);
+                if (deviceManager != null) {
+                    deviceManager.stop();
+                    mProfileDeviceManagers.remove(profileId);
+                    logd("Existing device manager removed for profile "
+                            + Utils.getProfileName(profileId));
+                }
 
-        for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
-            int key = mProfileDeviceManagers.keyAt(i);
-            BluetoothProfileDeviceManager deviceManager =
-                    (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
-            deviceManager.start();
+                deviceManager = BluetoothProfileDeviceManager.create(mContext, mUserId,
+                        mCarBluetoothUserService, profileId);
+                if (deviceManager == null) {
+                    logd("Failed to create profile device manager for "
+                            + Utils.getProfileName(profileId));
+                    continue;
+                }
+                mProfileDeviceManagers.put(profileId, deviceManager);
+                logd("Created profile device manager for " + Utils.getProfileName(profileId));
+            }
+
+            for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+                int key = mProfileDeviceManagers.keyAt(i);
+                BluetoothProfileDeviceManager deviceManager =
+                        (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+                deviceManager.start();
+            }
         }
     }
 
     /**
      * Stops and clears the entire set of Profile Device Managers.
      */
-    private synchronized void destroyBluetoothProfileDeviceManagers() {
-        for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
-            int key = mProfileDeviceManagers.keyAt(i);
-            BluetoothProfileDeviceManager deviceManager =
-                    (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
-            deviceManager.stop();
+    private void destroyBluetoothProfileDeviceManagers() {
+        synchronized (mPerUserLock) {
+            for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+                int key = mProfileDeviceManagers.keyAt(i);
+                BluetoothProfileDeviceManager deviceManager =
+                        (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+                deviceManager.stop();
+            }
+            mProfileDeviceManagers.clear();
         }
-        mProfileDeviceManagers.clear();
     }
 
     /**
      * Creates an instance of a BluetoothProfileInhibitManager under the current user
      */
-    private synchronized void createBluetoothProfileInhibitManager() {
+    private void createBluetoothProfileInhibitManager() {
         logd("Creating inhibit manager");
-        if (mUserId == -1) {
-            logd("No foreground user, cannot create profile inhibit manager");
-            return;
+        synchronized (mPerUserLock) {
+            if (mUserId == -1) {
+                logd("No foreground user, cannot create profile inhibit manager");
+                return;
+            }
+            mInhibitManager = new BluetoothProfileInhibitManager(mContext, mUserId,
+                    mCarBluetoothUserService);
+            mInhibitManager.start();
         }
-        mInhibitManager = new BluetoothProfileInhibitManager(mContext, mUserId,
-                mCarBluetoothUserService);
-        mInhibitManager.start();
     }
 
     /**
      * Destroys the current instance of a BluetoothProfileInhibitManager, if one exists
      */
-    private synchronized void destroyBluetoothProfileInhibitManager() {
+    private void destroyBluetoothProfileInhibitManager() {
         logd("Destroying inhibit manager");
-        if (mInhibitManager == null) return;
-        mInhibitManager.stop();
-        mInhibitManager = null;
+        synchronized (mPerUserLock) {
+            if (mInhibitManager == null) return;
+            mInhibitManager.stop();
+            mInhibitManager = null;
+        }
     }
 
     /**
      * Creates an instance of a BluetoothDeviceConnectionPolicy under the current user
      */
-    private synchronized void createBluetoothDeviceConnectionPolicy() {
+    private void createBluetoothDeviceConnectionPolicy() {
         logd("Creating device connection policy");
-        if (mUserId == -1) {
-            logd("No foreground user, cannot create device connection policy");
-            return;
+        synchronized (mPerUserLock) {
+            if (mUserId == -1) {
+                logd("No foreground user, cannot create device connection policy");
+                return;
+            }
+            mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext,
+                    mUserId, this);
+            if (mBluetoothDeviceConnectionPolicy == null) {
+                logd("Failed to create default Bluetooth device connection policy.");
+                return;
+            }
+            mBluetoothDeviceConnectionPolicy.init();
         }
-        mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext, mUserId,
-                this);
-        if (mBluetoothDeviceConnectionPolicy == null) {
-            logd("Failed to create default Bluetooth device connection policy.");
-            return;
-        }
-        mBluetoothDeviceConnectionPolicy.init();
     }
 
     /**
      * Destroys the current instance of a BluetoothDeviceConnectionPolicy, if one exists
      */
-    private synchronized void destroyBluetoothDeviceConnectionPolicy() {
+    private void destroyBluetoothDeviceConnectionPolicy() {
         logd("Destroying device connection policy");
-        if (mBluetoothDeviceConnectionPolicy != null) {
-            mBluetoothDeviceConnectionPolicy.release();
-            mBluetoothDeviceConnectionPolicy = null;
+        synchronized (mPerUserLock) {
+            if (mBluetoothDeviceConnectionPolicy != null) {
+                mBluetoothDeviceConnectionPolicy.release();
+                mBluetoothDeviceConnectionPolicy = null;
+            }
         }
     }
 
@@ -322,7 +364,9 @@
      * @return true if the default policy is active, false otherwise
      */
     public boolean isUsingDefaultConnectionPolicy() {
-        return mBluetoothDeviceConnectionPolicy != null;
+        synchronized (mPerUserLock) {
+            return mBluetoothDeviceConnectionPolicy != null;
+        }
     }
 
    /**
@@ -332,7 +376,7 @@
     public void connectDevices() {
         enforceBluetoothAdminPermission();
         logd("Connect devices for each profile");
-        synchronized (this) {
+        synchronized (mPerUserLock) {
             for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
                 int key = mProfileDeviceManagers.keyAt(i);
                 BluetoothProfileDeviceManager deviceManager =
@@ -350,7 +394,7 @@
      */
     public List<BluetoothDevice> getProfileDevicePriorityList(int profile) {
         enforceBluetoothAdminPermission();
-        synchronized (this) {
+        synchronized (mPerUserLock) {
             BluetoothProfileDeviceManager deviceManager =
                     (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
             if (deviceManager != null) {
@@ -369,7 +413,7 @@
      */
     public int getDeviceConnectionPriority(int profile, BluetoothDevice device) {
         enforceBluetoothAdminPermission();
-        synchronized (this) {
+        synchronized (mPerUserLock) {
             BluetoothProfileDeviceManager deviceManager =
                     (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
             if (deviceManager != null) {
@@ -388,7 +432,7 @@
      */
     public void setDeviceConnectionPriority(int profile, BluetoothDevice device, int priority) {
         enforceBluetoothAdminPermission();
-        synchronized (this) {
+        synchronized (mPerUserLock) {
             BluetoothProfileDeviceManager deviceManager =
                     (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
             if (deviceManager != null) {
@@ -408,11 +452,13 @@
      *                owning the token dies, the request will automatically be released
      * @return True if the profile was successfully inhibited, false if an error occurred.
      */
-    synchronized boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
+    boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
         logd("Request profile inhibit: profile " + Utils.getProfileName(profile)
                 + ", device " + device.getAddress());
-        if (mInhibitManager == null) return false;
-        return mInhibitManager.requestProfileInhibit(device, profile, token);
+        synchronized (mPerUserLock) {
+            if (mInhibitManager == null) return false;
+            return mInhibitManager.requestProfileInhibit(device, profile, token);
+        }
     }
 
     /**
@@ -425,11 +471,13 @@
      *                {@link #requestBluetoothProfileInhibit}.
      * @return True if the request was released, false if an error occurred.
      */
-    synchronized boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
+    boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
         logd("Release profile inhibit: profile " + Utils.getProfileName(profile)
                 + ", device " + device.getAddress());
-        if (mInhibitManager == null) return false;
-        return mInhibitManager.releaseProfileInhibit(device, profile, token);
+        synchronized (mPerUserLock) {
+            if (mInhibitManager == null) return false;
+            return mInhibitManager.releaseProfileInhibit(device, profile, token);
+        }
     }
 
     /**
@@ -452,29 +500,31 @@
      * Print out the verbose debug status of this object
      */
     @Override
-    public synchronized void dump(PrintWriter writer) {
+    public void dump(PrintWriter writer) {
         writer.println("*" + TAG + "*");
-        writer.println("\tUser ID: " + mUserId);
-        writer.println("\tUser Proxies: " + (mCarBluetoothUserService != null ? "Yes" : "No"));
+        synchronized (mPerUserLock) {
+            writer.println("\tUser ID: " + mUserId);
+            writer.println("\tUser Proxies: " + (mCarBluetoothUserService != null ? "Yes" : "No"));
 
-        // Profile Device Manager statuses
-        for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
-            int key = mProfileDeviceManagers.keyAt(i);
-            BluetoothProfileDeviceManager deviceManager =
-                    (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
-            deviceManager.dump(writer, "\t");
-        }
+            // Profile Device Manager statuses
+            for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+                int key = mProfileDeviceManagers.keyAt(i);
+                BluetoothProfileDeviceManager deviceManager =
+                        (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+                deviceManager.dump(writer, "\t");
+            }
 
-        // Profile Inhibits
-        if (mInhibitManager != null) mInhibitManager.dump(writer, "\t");
-        else writer.println("\tBluetoothProfileInhibitManager: null");
+            // Profile Inhibits
+            if (mInhibitManager != null) mInhibitManager.dump(writer, "\t");
+            else writer.println("\tBluetoothProfileInhibitManager: null");
 
-        // Device Connection Policy
-        writer.println("\tUsing default policy? " + (mUseDefaultPolicy ? "Yes" : "No"));
-        if (mBluetoothDeviceConnectionPolicy == null) {
-            writer.println("\tBluetoothDeviceConnectionPolicy: null");
-        } else {
-            mBluetoothDeviceConnectionPolicy.dump(writer, "\t");
+            // Device Connection Policy
+            writer.println("\tUsing default policy? " + (mUseDefaultPolicy ? "Yes" : "No"));
+            if (mBluetoothDeviceConnectionPolicy == null) {
+                writer.println("\tBluetoothDeviceConnectionPolicy: null");
+            } else {
+                mBluetoothDeviceConnectionPolicy.dump(writer, "\t");
+            }
         }
     }
 
diff --git a/service/src/com/android/car/CarDrivingStateService.java b/service/src/com/android/car/CarDrivingStateService.java
index 584228d..66a2a7c 100644
--- a/service/src/com/android/car/CarDrivingStateService.java
+++ b/service/src/com/android/car/CarDrivingStateService.java
@@ -30,11 +30,15 @@
 import android.content.Context;
 import android.hardware.automotive.vehicle.V2_0.VehicleGear;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.PrintWriter;
 import java.util.LinkedList;
 import java.util.List;
@@ -60,6 +64,8 @@
             VehicleProperty.PERF_VEHICLE_SPEED,
             VehicleProperty.GEAR_SELECTION,
             VehicleProperty.PARKING_BRAKE_ON};
+    private final HandlerThread mClientDispatchThread;
+    private final Handler mClientDispatchHandler;
     private CarDrivingStateEvent mCurrentDrivingState;
     // For dumpsys logging
     private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>();
@@ -75,6 +81,9 @@
         mContext = context;
         mPropertyService = propertyService;
         mCurrentDrivingState = createDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN);
+        mClientDispatchThread = new HandlerThread("ClientDispatchThread");
+        mClientDispatchThread.start();
+        mClientDispatchHandler = new Handler(mClientDispatchThread.getLooper());
     }
 
     @Override
@@ -316,7 +325,8 @@
      * Handle events coming from {@link CarPropertyService}.  Compute the driving state, map it to
      * the corresponding UX Restrictions and dispatch the events to the registered clients.
      */
-    private synchronized void handlePropertyEvent(CarPropertyEvent event) {
+    @VisibleForTesting
+    synchronized void handlePropertyEvent(CarPropertyEvent event) {
         if (event.getEventType() != CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE) {
             return;
         }
@@ -387,9 +397,13 @@
             if (DBG) {
                 Log.d(TAG, "dispatching to " + mDrivingStateClients.size() + " clients");
             }
-            for (DrivingStateClient client : mDrivingStateClients) {
-                client.dispatchEventToClients(mCurrentDrivingState);
-            }
+            // Dispatch to clients on a separate thread to prevent a deadlock
+            final CarDrivingStateEvent currentDrivingStateEvent = mCurrentDrivingState;
+            mClientDispatchHandler.post(() -> {
+                for (DrivingStateClient client : mDrivingStateClients) {
+                    client.dispatchEventToClients(currentDrivingStateEvent);
+                }
+            });
         }
     }
 
diff --git a/service/src/com/android/car/CarLocalServices.java b/service/src/com/android/car/CarLocalServices.java
index 6c756b8..ea1b6f1 100644
--- a/service/src/com/android/car/CarLocalServices.java
+++ b/service/src/com/android/car/CarLocalServices.java
@@ -17,6 +17,7 @@
 package com.android.car;
 
 import android.annotation.Nullable;
+import android.car.Car;
 import android.car.hardware.power.CarPowerManager;
 import android.content.Context;
 import android.util.ArrayMap;
@@ -90,10 +91,12 @@
      */
     @Nullable
     public static CarPowerManager createCarPowerManager(Context context) {
+        // This does not require connection as binder will be passed to CarPowerManager directly.
+        Car car = new Car(context, /* service= */null, /* handler= */ null);
         CarPowerManagementService service = getService(CarPowerManagementService.class);
         if (service == null) {
             return null;
         }
-        return new CarPowerManager(service, context, null);
+        return new CarPowerManager(car, service);
     }
 }
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 4841b0d..a2f5567 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -77,6 +77,13 @@
     private static final String SHARED_PREF = "com.android.car.media.car_media_service";
     private static final String COMPONENT_NAME_SEPARATOR = ",";
     private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION";
+    private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay";
+
+    // XML configuration options for autoplay on media source change.
+    private static final int AUTOPLAY_CONFIG_NEVER = 0;
+    private static final int AUTOPLAY_CONFIG_ALWAYS = 1;
+    // This mode uses the last stored playback state to determine whether to resume playback
+    private static final int AUTOPLAY_CONFIG_ADAPTIVE = 2;
 
     private final Context mContext;
     private final UserManager mUserManager;
@@ -89,8 +96,8 @@
     // null if playback has not been started yet.
     private MediaController mActiveUserMediaController;
     private SessionChangedListener mSessionsListener;
-    private boolean mStartPlayback;
-    private boolean mPlayOnMediaSourceChanged;
+    private int mPlayOnMediaSourceChangedConfig;
+    private int mPlayOnBootConfig;
 
     private boolean mPendingInit;
     private int mCurrentUser;
@@ -148,11 +155,7 @@
             if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
                 Log.d(CarLog.TAG_MEDIA, "Switched to user " + mCurrentUser);
             }
-            if (mUserManager.isUserUnlocked(mCurrentUser)) {
-                initUser();
-            } else {
-                mPendingInit = true;
-            }
+            maybeInitUser();
         }
     };
 
@@ -175,23 +178,36 @@
         userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
         mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
 
-        mPlayOnMediaSourceChanged =
-                mContext.getResources().getBoolean(R.bool.autoPlayOnMediaSourceChanged);
+        mPlayOnMediaSourceChangedConfig =
+                mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay);
+        mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay);
         mCurrentUser = ActivityManager.getCurrentUser();
-        updateMediaSessionCallbackForCurrentUser();
     }
 
     @Override
+    // This method is called from ICarImpl after CarMediaService is created.
     public void init() {
-        // Nothing to do. Reason: this method is only called once after rebooting, but we need to
-        // init user state each time a new user is unlocked, so this method is not the right
-        // place to call initUser().
+        maybeInitUser();
+    }
+
+    private void maybeInitUser() {
+        if (mUserManager.isUserUnlocked(mCurrentUser)) {
+            initUser();
+        } else {
+            mPendingInit = true;
+        }
     }
 
     private void initUser() {
+        // SharedPreferences are shared among different users thus only need initialized once. And
+        // they should be initialized after user 0 is unlocked because SharedPreferences in
+        // credential encrypted storage are not available until after user 0 is unlocked.
+        // initUser() is called when the current foreground user is unlocked, and by that time user
+        // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
         if (mSharedPrefs == null) {
             mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
         }
+
         if (mIsPackageUpdateReceiverRegistered) {
             mContext.unregisterReceiver(mPackageUpdateReceiver);
         }
@@ -200,22 +216,51 @@
                 mPackageUpdateFilter, null, null);
         mIsPackageUpdateReceiverRegistered = true;
 
-        mPrimaryMediaComponent = getLastMediaSource();
+        mPrimaryMediaComponent =
+                isCurrentUserEphemeral() ? getDefaultMediaSource() : getLastMediaSource();
         mActiveUserMediaController = null;
-        String key = PLAYBACK_STATE_KEY + mCurrentUser;
-        mStartPlayback =
-                mSharedPrefs.getInt(key, PlaybackState.STATE_NONE) == PlaybackState.STATE_PLAYING;
+
         updateMediaSessionCallbackForCurrentUser();
         notifyListeners();
 
-        // Start a service on the current user that binds to the media browser of the current media
-        // source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
-        // provide an API to connect on a specific user.
+        startMediaConnectorService(shouldStartPlayback(mPlayOnBootConfig), currentUser);
+    }
+
+    /**
+     * Starts a service on the current user that binds to the media browser of the current media
+     * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
+     * provide an API to connect on a specific user. Additionally, this service will attempt to
+     * resume playback using the MediaSession obtained via the media browser connection, which
+     * is more reliable than using active MediaSessions from MediaSessionManager.
+     */
+    private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {
         Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);
         serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));
+        serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);
         mContext.startForegroundServiceAsUser(serviceStart, currentUser);
     }
 
+    private boolean sharedPrefsInitialized() {
+        if (mSharedPrefs == null) {
+            // It shouldn't reach this but let's be cautious.
+            Log.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
+            String className = getClass().getName();
+            for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
+                // Let's print the useful logs only.
+                String log = ste.toString();
+                if (log.contains(className)) {
+                    Log.e(CarLog.TAG_MEDIA, log);
+                }
+            }
+            return false;
+        }
+        return true;
+    }
+
+    private boolean isCurrentUserEphemeral() {
+        return mUserManager.getUserInfo(mCurrentUser).isEphemeral();
+    }
+
     @Override
     public void release() {
         mMediaSessionUpdater.unregisterCallbacks();
@@ -297,10 +342,9 @@
                 if (!unlocked) {
                     return;
                 }
-                // No need to handle user0, non current foreground user, or ephemeral user.
+                // No need to handle user0, non current foreground user.
                 if (userHandle == UserHandle.USER_SYSTEM
-                        || userHandle != ActivityManager.getCurrentUser()
-                        || mUserManager.getUserInfo(userHandle).isEphemeral()) {
+                        || userHandle != ActivityManager.getCurrentUser()) {
                     return;
                 }
                 if (mPendingInit) {
@@ -474,23 +518,23 @@
 
         stop();
 
-        mStartPlayback = mPlayOnMediaSourceChanged;
         mPreviousMediaComponent = mPrimaryMediaComponent;
         mPrimaryMediaComponent = componentName;
         updateActiveMediaController(mMediaSessionManager
                 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
 
-        if (mSharedPrefs != null) {
-            if (mPrimaryMediaComponent != null && !TextUtils.isEmpty(
-                    mPrimaryMediaComponent.flattenToString())) {
+        if (mPrimaryMediaComponent != null && !TextUtils.isEmpty(
+                mPrimaryMediaComponent.flattenToString())) {
+            if (!isCurrentUserEphemeral()) {
                 saveLastMediaSource(mPrimaryMediaComponent);
-                mRemovedMediaSourcePackage = null;
             }
-        } else {
-            // Shouldn't reach this unless there is some other error in CarService
-            Log.e(CarLog.TAG_MEDIA, "Error trying to save last media source, prefs uninitialized");
+            mRemovedMediaSourcePackage = null;
         }
+
         notifyListeners();
+        if (shouldStartPlayback(mPlayOnMediaSourceChangedConfig)) {
+            startMediaConnectorService(true, new UserHandle(mCurrentUser));
+        }
     }
 
     private void notifyListeners() {
@@ -509,9 +553,9 @@
     private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
         @Override
         public void onPlaybackStateChanged(PlaybackState state) {
-            savePlaybackState(state);
-            // Try to start playback if the new state allows the play action
-            maybeRestartPlayback(state);
+            if (!isCurrentUserEphemeral()) {
+                savePlaybackState(state);
+            }
         }
     };
 
@@ -564,7 +608,6 @@
         return false;
     }
 
-
     private boolean isMediaService(@NonNull ComponentName componentName) {
         return getMediaService(componentName) != null;
     }
@@ -612,6 +655,9 @@
     }
 
     private void saveLastMediaSource(@NonNull ComponentName component) {
+        if (!sharedPrefsInitialized()) {
+            return;
+        }
         String componentName = component.flattenToString();
         String key = SOURCE_KEY + mCurrentUser;
         String serialized = mSharedPrefs.getString(key, null);
@@ -627,17 +673,22 @@
     }
 
     private ComponentName getLastMediaSource() {
-        String key = SOURCE_KEY + mCurrentUser;
-        String serialized = mSharedPrefs.getString(key, null);
-        if (!TextUtils.isEmpty(serialized)) {
-            for (String name : getComponentNameList(serialized)) {
-                ComponentName componentName = ComponentName.unflattenFromString(name);
-                if (isMediaService(componentName)) {
-                    return componentName;
+        if (sharedPrefsInitialized()) {
+            String key = SOURCE_KEY + mCurrentUser;
+            String serialized = mSharedPrefs.getString(key, null);
+            if (!TextUtils.isEmpty(serialized)) {
+                for (String name : getComponentNameList(serialized)) {
+                    ComponentName componentName = ComponentName.unflattenFromString(name);
+                    if (isMediaService(componentName)) {
+                        return componentName;
+                    }
                 }
             }
         }
+        return getDefaultMediaSource();
+    }
 
+    private ComponentName getDefaultMediaSource() {
         String defaultMediaSource = mContext.getString(R.string.default_media_source);
         ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource);
         if (isMediaService(defaultComponent)) {
@@ -656,24 +707,20 @@
     }
 
     private void savePlaybackState(PlaybackState playbackState) {
+        if (!sharedPrefsInitialized()) {
+            return;
+        }
         int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE;
-        if (state == PlaybackState.STATE_PLAYING) {
-            // No longer need to request play if audio was resumed already via some other means,
-            // e.g. Assistant starts playback, user uses hardware button, etc.
-            mStartPlayback = false;
-        }
-        if (mSharedPrefs != null) {
-            String key = PLAYBACK_STATE_KEY + mCurrentUser;
-            mSharedPrefs.edit().putInt(key, state).apply();
-        }
+        String key = getPlaybackStateKey();
+        mSharedPrefs.edit().putInt(key, state).apply();
     }
 
-    private void maybeRestartPlayback(PlaybackState state) {
-        if (mStartPlayback && state != null
-                && (state.getActions() & PlaybackState.ACTION_PLAY) != 0) {
-            play();
-            mStartPlayback = false;
-        }
+    /**
+     * Builds a string key for saving the playback state for a specific media source (and user)
+     */
+    private String getPlaybackStateKey() {
+        return PLAYBACK_STATE_KEY + mCurrentUser
+                + (mPrimaryMediaComponent == null ? "" : mPrimaryMediaComponent.flattenToString());
     }
 
     /**
@@ -691,19 +738,42 @@
         for (MediaController controller : mediaControllers) {
             if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller))) {
                 mActiveUserMediaController = controller;
+                PlaybackState state = mActiveUserMediaController.getPlaybackState();
+                if (!isCurrentUserEphemeral()) {
+                    savePlaybackState(state);
+                }
                 // Specify Handler to receive callbacks on, to avoid defaulting to the calling
                 // thread; this method can be called from the MediaSessionManager callback.
                 // Using the version of this method without passing a handler causes a
                 // RuntimeException for failing to create a Handler.
-                PlaybackState state = mActiveUserMediaController.getPlaybackState();
-                savePlaybackState(state);
                 mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler);
-                maybeRestartPlayback(state);
                 return;
             }
         }
     }
 
+    /**
+     * Returns whether we should autoplay the current media source
+     */
+    private boolean shouldStartPlayback(int config) {
+        switch (config) {
+            case AUTOPLAY_CONFIG_NEVER:
+                return false;
+            case AUTOPLAY_CONFIG_ALWAYS:
+                return true;
+            case AUTOPLAY_CONFIG_ADAPTIVE:
+                if (!sharedPrefsInitialized()) {
+                    return false;
+                }
+                return mSharedPrefs.getInt(getPlaybackStateKey(), PlaybackState.STATE_NONE)
+                        == PlaybackState.STATE_PLAYING;
+            default:
+                Log.e(CarLog.TAG_MEDIA, "Unsupported playback configuration: " + config);
+                return false;
+        }
+
+    }
+
     @NonNull
     private static String getClassName(@NonNull MediaController controller) {
         Bundle sessionExtras = controller.getExtras();
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index c1341da..83c5051 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -54,6 +54,10 @@
  */
 public class CarPowerManagementService extends ICarPower.Stub implements
         CarServiceBase, PowerHalService.PowerEventListener {
+
+    private final Object mLock = new Object();
+    private final Object mSimulationWaitObject = new Object();
+
     private final Context mContext;
     private final PowerHalService mHal;
     private final SystemInterface mSystemInterface;
@@ -62,32 +66,38 @@
     // The listeners that must indicate asynchronous completion by calling finished().
     private final PowerManagerCallbackList mPowerManagerListenersWithCompletion =
                           new PowerManagerCallbackList();
-    private final Set<IBinder> mListenersWeAreWaitingFor = new HashSet<>();
-    private final Object mSimulationSleepObject = new Object();
 
-    @GuardedBy("this")
+    @GuardedBy("mSimulationWaitObject")
+    private boolean mWakeFromSimulatedSleep;
+    @GuardedBy("mSimulationWaitObject")
+    private boolean mInSimulatedDeepSleepMode;
+
+    @GuardedBy("mLock")
+    private final Set<IBinder> mListenersWeAreWaitingFor = new HashSet<>();
+    @GuardedBy("mLock")
     private CpmsState mCurrentState;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private Timer mTimer;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private long mProcessingStartTime;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private long mLastSleepEntryTime;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private final LinkedList<CpmsState> mPendingPowerStates = new LinkedList<>();
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private HandlerThread mHandlerThread;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private PowerHandler mHandler;
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private boolean mTimerActive;
-    @GuardedBy("mSimulationSleepObject")
-    private boolean mInSimulatedDeepSleepMode = false;
-    @GuardedBy("mSimulationSleepObject")
-    private boolean mWakeFromSimulatedSleep = false;
-    private int mNextWakeupSec = 0;
-    private boolean mShutdownOnFinish = false;
+    @GuardedBy("mLock")
+    private int mNextWakeupSec;
+    @GuardedBy("mLock")
+    private boolean mShutdownOnFinish;
+    @GuardedBy("mLock")
     private boolean mIsBooting = true;
+    @GuardedBy("mLock")
+    private boolean mIsResuming;
 
     private final CarUserManagerHelper mCarUserManagerHelper;
 
@@ -160,7 +170,7 @@
 
     @Override
     public void init() {
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mHandlerThread = new HandlerThread(CarLog.TAG_POWER);
             mHandlerThread.start();
             mHandler = new PowerHandler(mHandlerThread.getLooper());
@@ -180,11 +190,12 @@
     @Override
     public void release() {
         HandlerThread handlerThread;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             releaseTimerLocked();
             mCurrentState = null;
             mHandler.cancelAll();
             handlerThread = mHandlerThread;
+            mListenersWeAreWaitingFor.clear();
         }
         handlerThread.quitSafely();
         try {
@@ -194,7 +205,6 @@
         }
         mSystemInterface.stopDisplayStateMonitoring();
         mPowerManagerListeners.kill();
-        mListenersWeAreWaitingFor.clear();
         mSystemInterface.releaseAllWakeLocks();
     }
 
@@ -212,7 +222,7 @@
     @Override
     public void onApPowerStateChange(PowerState state) {
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mPendingPowerStates.addFirst(new CpmsState(state));
             handler = mHandler;
         }
@@ -220,8 +230,11 @@
     }
 
     @VisibleForTesting
-    protected void clearIsBooting() {
-        mIsBooting = false;
+    protected void clearIsBootingOrResuming() {
+        synchronized (mLock) {
+            mIsBooting = false;
+            mIsResuming = false;
+        }
     }
 
     /**
@@ -230,7 +243,7 @@
     private void onApPowerStateChange(int apState, int carPowerStateListenerState) {
         CpmsState newState = new CpmsState(apState, carPowerStateListenerState);
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mPendingPowerStates.addFirst(newState);
             handler = mHandler;
         }
@@ -240,7 +253,7 @@
     private void doHandlePowerStateChange() {
         CpmsState state;
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             state = mPendingPowerStates.peekFirst();
             mPendingPowerStates.clear();
             if (state == null) {
@@ -304,10 +317,30 @@
     }
 
     private void handleOn() {
-        // Do not switch user if it is booting as there can be a race with CarServiceHelperService
-        if (mIsBooting) {
-            mIsBooting = false;
-        } else {
+        // Some OEMs have their own user-switching logic, which may not be coordinated with this
+        // code. To avoid contention, we don't switch users when we coming alive. The OEM's code
+        // should do the switch.
+        boolean allowUserSwitch = true;
+        synchronized (mLock) {
+            if (mIsBooting) {
+                // The system is booting, so don't switch users
+                allowUserSwitch = false;
+                mIsBooting = false;
+                mIsResuming = false;
+                Log.i(CarLog.TAG_POWER, "User switch disallowed while booting");
+            } else if (mIsResuming) {
+                // The system is resuming after a suspension. Optionally disable user switching.
+                allowUserSwitch = !mContext.getResources()
+                        .getBoolean(R.bool.config_disableUserSwitchDuringResume);
+                mIsBooting = false;
+                mIsResuming = false;
+                if (!allowUserSwitch) {
+                    Log.i(CarLog.TAG_POWER, "User switch disallowed while resuming");
+                }
+            }
+        }
+
+        if (allowUserSwitch) {
             int targetUserId = mCarUserManagerHelper.getInitialUser();
             if (targetUserId != UserHandle.USER_SYSTEM
                     && targetUserId != mCarUserManagerHelper.getCurrentForegroundUserId()) {
@@ -323,9 +356,11 @@
     private void handleShutdownPrepare(CpmsState newState) {
         mSystemInterface.setDisplayState(false);
         // Shutdown on finish if the system doesn't support deep sleep or doesn't allow it.
-        mShutdownOnFinish |= !mHal.isDeepSleepAllowed()
-                || !mSystemInterface.isSystemSupportingDeepSleep()
-                || !newState.mCanSleep;
+        synchronized (mLock) {
+            mShutdownOnFinish |= !mHal.isDeepSleepAllowed()
+                    || !mSystemInterface.isSystemSupportingDeepSleep()
+                    || !newState.mCanSleep;
+        }
         if (newState.mCanPostpone) {
             Log.i(CarLog.TAG_POWER, "starting shutdown prepare");
             sendPowerManagerEvent(CarPowerStateListener.SHUTDOWN_PREPARE);
@@ -333,7 +368,7 @@
             doHandlePreprocessing();
         } else {
             Log.i(CarLog.TAG_POWER, "starting shutdown immediately");
-            synchronized (CarPowerManagementService.this) {
+            synchronized (mLock) {
                 releaseTimerLocked();
             }
             // Notify hal that we are shutting down and since it is immediate, don't schedule next
@@ -355,21 +390,27 @@
 
     private void handleWaitForFinish(CpmsState state) {
         sendPowerManagerEvent(state.mCarPowerStateListenerState);
+        int wakeupSec;
+        synchronized (mLock) {
+            wakeupSec = mNextWakeupSec;
+        }
         switch (state.mCarPowerStateListenerState) {
             case CarPowerStateListener.SUSPEND_ENTER:
-                mHal.sendSleepEntry(mNextWakeupSec);
+                mHal.sendSleepEntry(wakeupSec);
                 break;
             case CarPowerStateListener.SHUTDOWN_ENTER:
-                mHal.sendShutdownStart(mNextWakeupSec);
+                mHal.sendShutdownStart(wakeupSec);
                 break;
         }
     }
 
     private void handleFinish() {
-        boolean mustShutDown;
         boolean simulatedMode;
-        synchronized (mSimulationSleepObject) {
+        synchronized (mSimulationWaitObject) {
             simulatedMode = mInSimulatedDeepSleepMode;
+        }
+        boolean mustShutDown;
+        synchronized (mLock) {
             mustShutDown = mShutdownOnFinish && !simulatedMode;
         }
         if (mustShutDown) {
@@ -380,15 +421,13 @@
         }
     }
 
-    @GuardedBy("this")
+    @GuardedBy("mLock")
     private void releaseTimerLocked() {
-        synchronized (CarPowerManagementService.this) {
-            if (mTimer != null) {
-                mTimer.cancel();
-            }
-            mTimer = null;
-            mTimerActive = false;
+        if (mTimer != null) {
+            mTimer.cancel();
         }
+        mTimer = null;
+        mTimerActive = false;
     }
 
     private void doHandlePreprocessing() {
@@ -407,7 +446,7 @@
         }
         Log.i(CarLog.TAG_POWER, "processing before shutdown expected for: "
                 + sShutdownPrepareTimeMs + " ms, adding polling:" + pollingCount);
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mProcessingStartTime = SystemClock.elapsedRealtime();
             releaseTimerLocked();
             mTimer = new Timer();
@@ -433,7 +472,7 @@
         // see the list go empty and we will think that we are done.
         boolean haveSomeCompleters = false;
         PowerManagerCallbackList completingListeners = new PowerManagerCallbackList();
-        synchronized (mListenersWeAreWaitingFor) {
+        synchronized (mLock) {
             mListenersWeAreWaitingFor.clear();
             int idx = mPowerManagerListenersWithCompletion.beginBroadcast();
             while (idx-- > 0) {
@@ -475,28 +514,33 @@
         // enterDeepSleep should force sleep entry even if wake lock is kept.
         mSystemInterface.switchToPartialWakeLock();
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             handler = mHandler;
         }
         handler.cancelProcessingComplete();
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mLastSleepEntryTime = SystemClock.elapsedRealtime();
         }
         int nextListenerState;
         if (simulatedMode) {
-            simulateSleepByLooping();
+            simulateSleepByWaiting();
             nextListenerState = CarPowerStateListener.SHUTDOWN_CANCELLED;
         } else {
             boolean sleepSucceeded = mSystemInterface.enterDeepSleep();
             if (!sleepSucceeded) {
-                // VHAL should transition CPMS to shutdown.
+                // Suspend failed! VHAL should transition CPMS to shutdown.
                 Log.e(CarLog.TAG_POWER, "Sleep did not succeed. Now attempting to shut down.");
                 mSystemInterface.shutdown();
+                return;
             }
             nextListenerState = CarPowerStateListener.SUSPEND_EXIT;
         }
-        // On wake, reset nextWakeup time. If not set again, system will suspend/shutdown forever.
-        mNextWakeupSec = 0;
+        // On resume, reset nextWakeup time. If not set again, system will suspend/shutdown forever.
+        synchronized (mLock) {
+            mIsResuming = true;
+            mNextWakeupSec = 0;
+        }
+        Log.i(CarLog.TAG_POWER, "Resuming after suspending");
         mSystemInterface.refreshDisplayBrightness();
         onApPowerStateChange(CpmsState.WAIT_FOR_VHAL, nextListenerState);
     }
@@ -537,26 +581,24 @@
     }
 
     private void doHandleProcessingComplete() {
-        synchronized (CarPowerManagementService.this) {
+        int listenerState;
+        synchronized (mLock) {
             releaseTimerLocked();
             if (!mShutdownOnFinish && mLastSleepEntryTime > mProcessingStartTime) {
                 // entered sleep after processing start. So this could be duplicate request.
                 Log.w(CarLog.TAG_POWER, "Duplicate sleep entry request, ignore");
                 return;
             }
+            listenerState = mShutdownOnFinish
+                    ? CarPowerStateListener.SHUTDOWN_ENTER : CarPowerStateListener.SUSPEND_ENTER;
         }
-
-        if (mShutdownOnFinish) {
-            onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, CarPowerStateListener.SHUTDOWN_ENTER);
-        } else {
-            onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, CarPowerStateListener.SUSPEND_ENTER);
-        }
+        onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, listenerState);
     }
 
     @Override
     public void onDisplayBrightnessChange(int brightness) {
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             handler = mHandler;
         }
         handler.handleDisplayBrightnessChange(brightness);
@@ -572,7 +614,7 @@
 
     public void handleMainDisplayChanged(boolean on) {
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             handler = mHandler;
         }
         handler.handleMainDisplayStateChange(on);
@@ -586,8 +628,13 @@
         mHal.sendDisplayBrightness(brightness);
     }
 
-    public synchronized Handler getHandler() {
-        return mHandler;
+    /**
+     * Get the PowerHandler that we use to change power states
+     */
+    public Handler getHandler() {
+        synchronized (mLock) {
+            return mHandler;
+        }
     }
 
     // Binder interface for general use.
@@ -628,7 +675,9 @@
     @Override
     public void requestShutdownOnNextSuspend() {
         ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_POWER);
-        mShutdownOnFinish = true;
+        synchronized (mLock) {
+            mShutdownOnFinish = true;
+        }
     }
 
     @Override
@@ -639,27 +688,31 @@
     }
 
     @Override
-    public synchronized void scheduleNextWakeupTime(int seconds) {
+    public void scheduleNextWakeupTime(int seconds) {
         if (seconds < 0) {
-            Log.w(CarLog.TAG_POWER, "Next wake up can not be in negative time. Ignoring!");
+            Log.w(CarLog.TAG_POWER, "Next wake up time is negative. Ignoring!");
             return;
         }
-        if (!mHal.isTimedWakeupAllowed()) {
-            Log.w(CarLog.TAG_POWER, "Setting timed wakeups are disabled in HAL. Skipping");
-            mNextWakeupSec = 0;
-            return;
-        }
-        if (mNextWakeupSec == 0 || mNextWakeupSec > seconds) {
-            mNextWakeupSec = seconds;
-        } else {
-            Log.d(CarLog.TAG_POWER, "Tried to schedule next wake up, but already had shorter "
-                    + "scheduled time");
+        boolean timedWakeupAllowed = mHal.isTimedWakeupAllowed();
+        synchronized (mLock) {
+            if (!timedWakeupAllowed) {
+                Log.w(CarLog.TAG_POWER, "Setting timed wakeups are disabled in HAL. Skipping");
+                mNextWakeupSec = 0;
+                return;
+            }
+            if (mNextWakeupSec == 0 || mNextWakeupSec > seconds) {
+                // The new value is sooner than the old value. Take the new value.
+                mNextWakeupSec = seconds;
+            } else {
+                Log.d(CarLog.TAG_POWER, "Tried to schedule next wake up, but already had shorter "
+                        + "scheduled time");
+            }
         }
     }
 
     private void finishedImpl(IBinder binder) {
         boolean allAreComplete = false;
-        synchronized (mListenersWeAreWaitingFor) {
+        synchronized (mLock) {
             boolean oneWasRemoved = mListenersWeAreWaitingFor.remove(binder);
             allAreComplete = oneWasRemoved && mListenersWeAreWaitingFor.isEmpty();
         }
@@ -673,7 +726,7 @@
                 || mCurrentState.mState == CpmsState.SIMULATE_SLEEP) {
             PowerHandler powerHandler;
             // All apps are ready to shutdown/suspend.
-            synchronized (CarPowerManagementService.this) {
+            synchronized (mLock) {
                 if (!mShutdownOnFinish) {
                     if (mLastSleepEntryTime > mProcessingStartTime
                             && mLastSleepEntryTime < SystemClock.elapsedRealtime()) {
@@ -765,7 +818,7 @@
 
         @Override
         public void run() {
-            synchronized (CarPowerManagementService.this) {
+            synchronized (mLock) {
                 if (!mTimerActive) {
                     // Ignore timer expiration since we got cancelled
                     return;
@@ -927,9 +980,9 @@
         }
         handler.handlePowerStateChange();
 
-        synchronized (mSimulationSleepObject) {
+        synchronized (mSimulationWaitObject) {
             mWakeFromSimulatedSleep = true;
-            mSimulationSleepObject.notify();
+            mSimulationWaitObject.notify();
         }
     }
 
@@ -940,12 +993,12 @@
      * that is not directly derived from a VehicleApPowerStateReq.
      */
     public void forceSimulatedSuspend() {
-        synchronized (mSimulationSleepObject) {
+        synchronized (mSimulationWaitObject) {
             mInSimulatedDeepSleepMode = true;
             mWakeFromSimulatedSleep = false;
         }
         PowerHandler handler;
-        synchronized (this) {
+        synchronized (mLock) {
             mPendingPowerStates.addFirst(new CpmsState(CpmsState.SIMULATE_SLEEP,
                                                        CarPowerStateListener.SHUTDOWN_PREPARE));
             handler = mHandler;
@@ -956,19 +1009,20 @@
     // In a real Deep Sleep, the hardware removes power from the CPU (but retains power
     // on the RAM). This puts the processor to sleep. Upon some external signal, power
     // is re-applied to the CPU, and processing resumes right where it left off.
-    // We simulate this behavior by simply going into a loop.
-    // We exit the loop when forceResume() is called.
-    private void simulateSleepByLooping() {
-        Log.i(CarLog.TAG_POWER, "Starting to simulate Deep Sleep by looping");
-        synchronized (mSimulationSleepObject) {
+    // We simulate this behavior by calling wait().
+    // We continue from wait() when forceSimulatedResume() is called.
+    private void simulateSleepByWaiting() {
+        Log.i(CarLog.TAG_POWER, "Starting to simulate Deep Sleep by waiting");
+        synchronized (mSimulationWaitObject) {
             while (!mWakeFromSimulatedSleep) {
                 try {
-                    mSimulationSleepObject.wait();
+                    mSimulationWaitObject.wait();
                 } catch (InterruptedException ignored) {
+                    Thread.currentThread().interrupt(); // Restore interrupted status
                 }
             }
             mInSimulatedDeepSleepMode = false;
         }
-        Log.i(CarLog.TAG_POWER, "Exit Deep Sleep simulation loop");
+        Log.i(CarLog.TAG_POWER, "Exit Deep Sleep simulation");
     }
 }
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 6f9ae8d..3f8f84c 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -158,6 +158,20 @@
             }
         };
 
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
+            int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
+                    WIFI_AP_STATE_DISABLED);
+            int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
+            String ifaceName = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+            int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
+                    WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+            handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
+        }
+    };
+
     private boolean mBound;
     private Intent mRegisteredService;
 
@@ -645,6 +659,11 @@
             public void onStopped() {
                 Log.i(TAG, "Local-only hotspot stopped.");
                 synchronized (mLock) {
+                    if (mLocalOnlyHotspotReservation != null) {
+                        // We must explicitly released old reservation object, otherwise it may
+                        // unexpectedly stop LOHS later because it overrode finalize() method.
+                        mLocalOnlyHotspotReservation.close();
+                    }
                     mLocalOnlyHotspotReservation = null;
                 }
                 sendApStopped();
@@ -732,22 +751,7 @@
     @Override
     public void init() {
         mContext.registerReceiver(
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        final int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE,
-                                WIFI_AP_STATE_DISABLED);
-                        final int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
-                                WIFI_AP_STATE_DISABLED);
-                        final int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
-                        final String ifaceName =
-                                intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
-                        final int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
-                                WifiManager.IFACE_IP_MODE_UNSPECIFIED);
-                        handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
-                    }
-                },
-                new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
+                mBroadcastReceiver, new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
     }
 
     private void handleWifiApStateChange(int currState, int prevState, int errorCode,
@@ -774,6 +778,7 @@
         synchronized (mLock) {
             mKeyEventHandlers.clear();
         }
+        mContext.unregisterReceiver(mBroadcastReceiver);
     }
 
     @Override
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 3b4e388..c9a14c9 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -18,6 +18,7 @@
 
 import static java.lang.Integer.toHexString;
 
+import android.car.Car;
 import android.car.hardware.CarPropertyConfig;
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyEvent;
@@ -348,6 +349,10 @@
             return;
         }
         ICarImpl.assertPermission(mContext, mHal.getWritePermission(propId));
+        // need an extra permission for writing display units properties.
+        if (mHal.isDisplayUnitsProperty(propId)) {
+            ICarImpl.assertPermission(mContext, Car.PERMISSION_VENDOR_EXTENSION);
+        }
         mHal.setProperty(prop);
     }
 
diff --git a/service/src/com/android/car/CarService.java b/service/src/com/android/car/CarService.java
index 509ecdd..044c791 100644
--- a/service/src/com/android/car/CarService.java
+++ b/service/src/com/android/car/CarService.java
@@ -25,6 +25,7 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.IHwBinder.DeathRecipient;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
@@ -41,6 +42,8 @@
 
 public class CarService extends Service {
 
+    private static final boolean RESTART_CAR_SERVICE_WHEN_VHAL_CRASH = true;
+
     private static final long WAIT_FOR_VEHICLE_HAL_TIMEOUT_MS = 10_000;
 
     private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
@@ -96,6 +99,7 @@
         linkToDeath(mVehicle, mVehicleDeathRecipient);
 
         ServiceManager.addService("car_service", mICarImpl);
+        ServiceManager.addService("car_stats", mICarImpl.getStatsService());
         SystemProperties.set("boot.car_service_created", "1");
         super.onCreate();
     }
@@ -178,7 +182,13 @@
 
         @Override
         public void serviceDied(long cookie) {
-            Log.w(CarLog.TAG_SERVICE, "Vehicle HAL died.");
+            if (RESTART_CAR_SERVICE_WHEN_VHAL_CRASH) {
+                Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died. Car service will restart***");
+                Process.killProcess(Process.myPid());
+                return;
+            }
+
+            Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died.***");
 
             try {
                 mVehicle.unlinkToDeath(this);
diff --git a/service/src/com/android/car/CarServiceBase.java b/service/src/com/android/car/CarServiceBase.java
index e014cf0..c4b578d 100644
--- a/service/src/com/android/car/CarServiceBase.java
+++ b/service/src/com/android/car/CarServiceBase.java
@@ -36,7 +36,4 @@
     default void vehicleHalReconnected() {}
 
     void dump(PrintWriter writer);
-
-    /** Called when we only want to dump metrics instead of everything else. */
-    default void dumpMetrics(PrintWriter writer) {};
 }
diff --git a/service/src/com/android/car/CarTestService.java b/service/src/com/android/car/CarTestService.java
index 8c3f64d..d72d87d 100644
--- a/service/src/com/android/car/CarTestService.java
+++ b/service/src/com/android/car/CarTestService.java
@@ -22,6 +22,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -39,6 +41,9 @@
     private final Context mContext;
     private final ICarImpl mICarImpl;
 
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
     private final Map<IBinder, TokenDeathRecipient> mTokens = new HashMap<>();
 
     CarTestService(Context context, ICarImpl carImpl) {
@@ -69,7 +74,7 @@
         Log.d(TAG, "stopCarService, token: " + token);
         ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE);
 
-        synchronized (this) {
+        synchronized (mLock) {
             if (mTokens.containsKey(token)) {
                 Log.w(TAG, "Calling stopCarService twice with the same token.");
                 return;
@@ -80,7 +85,7 @@
             token.linkToDeath(deathRecipient, 0);
 
             if (mTokens.size() == 1) {
-                mICarImpl.release();
+                CarServiceUtils.runOnMainSync(mICarImpl::release);
             }
         }
     }
@@ -92,15 +97,17 @@
         releaseToken(token);
     }
 
-    private synchronized void releaseToken(IBinder token) {
+    private void releaseToken(IBinder token) {
         Log.d(TAG, "releaseToken, token: " + token);
-        DeathRecipient deathRecipient = mTokens.remove(token);
-        if (deathRecipient != null) {
-            token.unlinkToDeath(deathRecipient, 0);
-        }
+        synchronized (mLock) {
+            DeathRecipient deathRecipient = mTokens.remove(token);
+            if (deathRecipient != null) {
+                token.unlinkToDeath(deathRecipient, 0);
+            }
 
-        if (mTokens.size() == 0) {
-            CarServiceUtils.runOnMain(mICarImpl::init);
+            if (mTokens.size() == 0) {
+                CarServiceUtils.runOnMainSync(mICarImpl::init);
+            }
         }
     }
 
@@ -116,4 +123,4 @@
             releaseToken(mToken);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index fe7c4d6..d2fed46 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -18,12 +18,15 @@
 
 import android.annotation.MainThread;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.UiModeManager;
 import android.car.Car;
 import android.car.ICar;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.hardware.automotive.vehicle.V2_0.IVehicle;
@@ -37,12 +40,14 @@
 import android.util.Slog;
 import android.util.TimingsTraceLog;
 
+import com.android.car.am.FixedActivityService;
 import com.android.car.audio.CarAudioService;
 import com.android.car.cluster.InstrumentClusterService;
 import com.android.car.garagemode.GarageModeService;
 import com.android.car.hal.VehicleHal;
 import com.android.car.internal.FeatureConfiguration;
 import com.android.car.pm.CarPackageManagerService;
+import com.android.car.stats.CarStatsService;
 import com.android.car.systeminterface.SystemInterface;
 import com.android.car.trust.CarTrustedDeviceService;
 import com.android.car.user.CarUserNoticeService;
@@ -63,6 +68,7 @@
     public static final String INTERNAL_INPUT_SERVICE = "internal_input";
     public static final String INTERNAL_SYSTEM_ACTIVITY_MONITORING_SERVICE =
             "system_activity_monitoring";
+    public static final String INTERNAL_VMS_MANAGER = "vms_manager";
 
     private final Context mContext;
     private final VehicleHal mHal;
@@ -80,6 +86,7 @@
     private final CarPropertyService mCarPropertyService;
     private final CarNightService mCarNightService;
     private final AppFocusService mAppFocusService;
+    private final FixedActivityService mFixedActivityService;
     private final GarageModeService mGarageModeService;
     private final InstrumentClusterService mInstrumentClusterService;
     private final CarLocationService mCarLocationService;
@@ -99,6 +106,7 @@
     private final VmsSubscriberService mVmsSubscriberService;
     private final VmsPublisherService mVmsPublisherService;
     private final CarBugreportManagerService mCarBugreportManagerService;
+    private final CarStatsService mCarStatsService;
 
     private final CarServiceBase[] mAllServices;
 
@@ -149,18 +157,21 @@
         mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
         mCarAudioService = new CarAudioService(serviceContext);
         mCarNightService = new CarNightService(serviceContext, mCarPropertyService);
+        mFixedActivityService = new FixedActivityService(serviceContext);
         mInstrumentClusterService = new InstrumentClusterService(serviceContext,
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
                 serviceContext, mCarAudioService, this);
+        mCarStatsService = new CarStatsService(serviceContext);
         mVmsBrokerService = new VmsBrokerService();
         mVmsClientManager = new VmsClientManager(
-                serviceContext, mVmsBrokerService, mCarUserService, mUserManagerHelper,
+                // CarStatsService needs to be passed to the constructor due to HAL init order
+                serviceContext, mCarStatsService, mCarUserService, mVmsBrokerService,
                 mHal.getVmsHal());
         mVmsSubscriberService = new VmsSubscriberService(
                 serviceContext, mVmsBrokerService, mVmsClientManager, mHal.getVmsHal());
         mVmsPublisherService = new VmsPublisherService(
-                serviceContext, mVmsBrokerService, mVmsClientManager);
+                serviceContext, mCarStatsService, mVmsBrokerService, mVmsClientManager);
         mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
         mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
                 systemInterface);
@@ -177,6 +188,7 @@
         CarLocalServices.addService(SystemInterface.class, mSystemInterface);
         CarLocalServices.addService(CarDrivingStateService.class, mCarDrivingStateService);
         CarLocalServices.addService(PerUserCarServiceHelper.class, mPerUserCarServiceHelper);
+        CarLocalServices.addService(FixedActivityService.class, mFixedActivityService);
 
         // Be careful with order. Service depending on other service should be inited later.
         List<CarServiceBase> allServices = new ArrayList<>();
@@ -193,6 +205,7 @@
         allServices.add(mAppFocusService);
         allServices.add(mCarAudioService);
         allServices.add(mCarNightService);
+        allServices.add(mFixedActivityService);
         allServices.add(mInstrumentClusterService);
         allServices.add(mSystemStateControllerService);
         allServices.add(mPerUserCarServiceHelper);
@@ -231,7 +244,6 @@
             mAllServices[i].release();
         }
         mHal.release();
-        CarLocalServices.removeAllServices();
     }
 
     void vehicleHalReconnected(IVehicle vehicle) {
@@ -365,6 +377,8 @@
                 return mCarInputService;
             case INTERNAL_SYSTEM_ACTIVITY_MONITORING_SERVICE:
                 return mSystemActivityMonitoringService;
+            case INTERNAL_VMS_MANAGER:
+                return mVmsClientManager;
             default:
                 Log.w(CarLog.TAG_SERVICE, "getCarInternalService for unknown service:" +
                         serviceName);
@@ -372,6 +386,10 @@
         }
     }
 
+    CarStatsService getStatsService() {
+        return mCarStatsService;
+    }
+
     public static void assertVehicleHalMockPermission(Context context) {
         assertPermission(context, Car.PERMISSION_MOCK_VEHICLE_HAL);
     }
@@ -463,7 +481,7 @@
             writer.println("*FutureConfig, DEFAULT:" + FeatureConfiguration.DEFAULT);
             writer.println("*Dump all services*");
 
-            dumpAllServices(writer, false);
+            dumpAllServices(writer);
 
             writer.println("*Dump Vehicle HAL*");
             writer.println("Vehicle HAL Interface: " + mVehicleInterfaceName);
@@ -475,8 +493,8 @@
                 e.printStackTrace(writer);
             }
         } else if ("--metrics".equals(args[0])) {
-            writer.println("*Dump car service metrics*");
-            dumpAllServices(writer, true);
+            // Strip the --metrics flag when passing dumpsys arguments to CarStatsService
+            mCarStatsService.dump(fd, writer, Arrays.copyOfRange(args, 1, args.length));
         } else if ("--vms-hal".equals(args[0])) {
             mHal.getVmsHal().dumpMetrics(fd);
         } else if (Build.IS_USERDEBUG || Build.IS_ENG) {
@@ -486,23 +504,18 @@
         }
     }
 
-    private void dumpAllServices(PrintWriter writer, boolean dumpMetricsOnly) {
+    private void dumpAllServices(PrintWriter writer) {
         for (CarServiceBase service : mAllServices) {
-            dumpService(service, writer, dumpMetricsOnly);
+            dumpService(service, writer);
         }
         if (mCarTestService != null) {
-            dumpService(mCarTestService, writer, dumpMetricsOnly);
+            dumpService(mCarTestService, writer);
         }
-
     }
 
-    private void dumpService(CarServiceBase service, PrintWriter writer, boolean dumpMetricsOnly) {
+    private void dumpService(CarServiceBase service, PrintWriter writer) {
         try {
-            if (dumpMetricsOnly) {
-                service.dumpMetrics(writer);
-            } else {
-                service.dump(writer);
-            }
+            service.dump(writer);
         } catch (Exception e) {
             writer.println("Failed dumping: " + service.getClass().getName());
             e.printStackTrace(writer);
@@ -540,6 +553,8 @@
         private static final String COMMAND_SUSPEND = "suspend";
         private static final String COMMAND_ENABLE_TRUSTED_DEVICE = "enable-trusted-device";
         private static final String COMMAND_REMOVE_TRUSTED_DEVICES = "remove-trusted-devices";
+        private static final String COMMAND_START_FIXED_ACTIVITY_MODE = "start-fixed-activity-mode";
+        private static final String COMMAND_STOP_FIXED_ACTIVITY_MODE = "stop-fixed-activity-mode";
 
         private static final String PARAM_DAY_MODE = "day";
         private static final String PARAM_NIGHT_MODE = "night";
@@ -584,6 +599,11 @@
                     + " wireless projection");
             pw.println("\t--metrics");
             pw.println("\t  When used with dumpsys, only metrics will be in the dumpsys output.");
+            pw.println("\tstart-fixed-activity displayId packageName activityName");
+            pw.println("\t  Start an Activity the specified display as fixed mode");
+            pw.println("\tstop-fixed-mode displayId");
+            pw.println("\t  Stop fixed Activity mode for the given display. "
+                    + "The Activity will not be restarted upon crash.");
         }
 
         public void exec(String[] args, PrintWriter writer) {
@@ -710,12 +730,62 @@
                             .removeAllTrustedDevices(
                                     mUserManagerHelper.getCurrentForegroundUserId());
                     break;
+                case COMMAND_START_FIXED_ACTIVITY_MODE:
+                    handleStartFixedActivity(args, writer);
+                    break;
+                case COMMAND_STOP_FIXED_ACTIVITY_MODE:
+                    handleStopFixedMode(args, writer);
+                    break;
                 default:
                     writer.println("Unknown command: \"" + arg + "\"");
                     dumpHelp(writer);
             }
         }
 
+        private void handleStartFixedActivity(String[] args, PrintWriter writer) {
+            if (args.length != 4) {
+                writer.println("Incorrect number of arguments");
+                dumpHelp(writer);
+                return;
+            }
+            int displayId;
+            try {
+                displayId = Integer.parseInt(args[1]);
+            } catch (NumberFormatException e) {
+                writer.println("Wrong display id:" + args[1]);
+                return;
+            }
+            String packageName = args[2];
+            String activityName = args[3];
+            Intent intent = new Intent();
+            intent.setComponent(new ComponentName(packageName, activityName));
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            ActivityOptions options = ActivityOptions.makeBasic();
+            options.setLaunchDisplayId(displayId);
+            if (!mFixedActivityService.startFixedActivityModeForDisplayAndUser(intent, options,
+                    displayId, ActivityManager.getCurrentUser())) {
+                writer.println("Failed to start");
+                return;
+            }
+            writer.println("Succeeded");
+        }
+
+        private void handleStopFixedMode(String[] args, PrintWriter writer) {
+            if (args.length != 2) {
+                writer.println("Incorrect number of arguments");
+                dumpHelp(writer);
+                return;
+            }
+            int displayId;
+            try {
+                displayId = Integer.parseInt(args[1]);
+            } catch (NumberFormatException e) {
+                writer.println("Wrong display id:" + args[1]);
+                return;
+            }
+            mFixedActivityService.stopFixedActivityMode(displayId);
+        }
+
         private void forceDayNightMode(String arg, PrintWriter writer) {
             int mode;
             switch (arg) {
diff --git a/service/src/com/android/car/OnShutdownReboot.java b/service/src/com/android/car/OnShutdownReboot.java
index b68ff5b..5a14371 100644
--- a/service/src/com/android/car/OnShutdownReboot.java
+++ b/service/src/com/android/car/OnShutdownReboot.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.BiConsumer;
 
@@ -52,10 +53,10 @@
 
     OnShutdownReboot(Context context) {
         mContext = context;
-        IntentFilter shutdownFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
-        IntentFilter rebootFilter = new IntentFilter(Intent.ACTION_REBOOT);
-        mContext.registerReceiver(mReceiver, shutdownFilter);
-        mContext.registerReceiver(mReceiver, rebootFilter);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_SHUTDOWN);
+        filter.addAction(Intent.ACTION_REBOOT);
+        mContext.registerReceiver(mReceiver, filter);
     }
 
     OnShutdownReboot addAction(BiConsumer<Context, Intent> action) {
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 3029a9e..da0b8b7 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -29,16 +29,16 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.car.stats.CarStatsService;
 import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.util.Collections;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
+import java.util.function.IntSupplier;
 
 
 /**
@@ -50,75 +50,35 @@
     private static final boolean DBG = false;
     private static final String TAG = "VmsPublisherService";
 
-    @VisibleForTesting
-    static final String PACKET_COUNT_FORMAT = "Packet count for layer %s: %d\n";
-
-    @VisibleForTesting
-    static final String PACKET_SIZE_FORMAT = "Total packet size for layer %s: %d (bytes)\n";
-
-    @VisibleForTesting
-    static final String PACKET_FAILURE_COUNT_FORMAT =
-            "Total packet failure count for layer %s from %s to %s: %d\n";
-
-    @VisibleForTesting
-    static final String PACKET_FAILURE_SIZE_FORMAT =
-            "Total packet failure size for layer %s from %s to %s: %d (bytes)\n";
-
     private final Context mContext;
+    private final CarStatsService mStatsService;
     private final VmsBrokerService mBrokerService;
     private final VmsClientManager mClientManager;
+    private final IntSupplier mGetCallingUid;
     private final Map<String, PublisherProxy> mPublisherProxies = Collections.synchronizedMap(
             new ArrayMap<>());
 
-    @GuardedBy("mPacketCounts")
-    private final Map<VmsLayer, PacketCountAndSize> mPacketCounts = new ArrayMap<>();
-    @GuardedBy("mPacketFailureCounts")
-    private final Map<PacketFailureKey, PacketCountAndSize> mPacketFailureCounts = new ArrayMap<>();
-
-    // PacketCountAndSize keeps track of the cumulative size and number of packets of a specific
-    // VmsLayer that we have seen.
-    private class PacketCountAndSize {
-        long mCount;
-        long mSize;
-    }
-
-    // PacketFailureKey is a triple of the VmsLayer, the publisher and subscriber for which a packet
-    // failed to be sent.
-    private class PacketFailureKey {
-        VmsLayer mVmsLayer;
-        String mPublisher;
-        String mSubscriber;
-
-        PacketFailureKey(VmsLayer vmsLayer, String publisher, String subscriber) {
-            mVmsLayer = vmsLayer;
-            mPublisher = publisher;
-            mSubscriber = subscriber;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (!(o instanceof PacketFailureKey)) {
-                return false;
-            }
-
-            PacketFailureKey otherKey = (PacketFailureKey) o;
-            return Objects.equals(mVmsLayer, otherKey.mVmsLayer) && Objects.equals(mPublisher,
-                    otherKey.mPublisher) && Objects.equals(mSubscriber, otherKey.mSubscriber);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mVmsLayer, mPublisher, mSubscriber);
-        }
-    }
-
-    public VmsPublisherService(
+    VmsPublisherService(
             Context context,
+            CarStatsService statsService,
             VmsBrokerService brokerService,
             VmsClientManager clientManager) {
+        this(context, statsService, brokerService, clientManager, Binder::getCallingUid);
+    }
+
+    @VisibleForTesting
+    VmsPublisherService(
+            Context context,
+            CarStatsService statsService,
+            VmsBrokerService brokerService,
+            VmsClientManager clientManager,
+            IntSupplier getCallingUid) {
         mContext = context;
+        mStatsService = statsService;
         mBrokerService = brokerService;
         mClientManager = clientManager;
+        mGetCallingUid = getCallingUid;
+
         mClientManager.setPublisherService(this);
     }
 
@@ -133,35 +93,8 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        dumpMetrics(writer);
-    }
-
-    @Override
-    public void dumpMetrics(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
         writer.println("mPublisherProxies: " + mPublisherProxies.size());
-        synchronized (mPacketCounts) {
-            for (Map.Entry<VmsLayer, PacketCountAndSize> entry : mPacketCounts.entrySet()) {
-                VmsLayer layer = entry.getKey();
-                PacketCountAndSize countAndSize = entry.getValue();
-                writer.format(PACKET_COUNT_FORMAT, layer, countAndSize.mCount);
-                writer.format(PACKET_SIZE_FORMAT, layer, countAndSize.mSize);
-            }
-        }
-        synchronized (mPacketFailureCounts) {
-            for (Map.Entry<PacketFailureKey, PacketCountAndSize> entry :
-                    mPacketFailureCounts.entrySet()) {
-                PacketFailureKey key = entry.getKey();
-                PacketCountAndSize countAndSize = entry.getValue();
-                VmsLayer layer = key.mVmsLayer;
-                String publisher = key.mPublisher;
-                String subscriber = key.mSubscriber;
-                writer.format(PACKET_FAILURE_COUNT_FORMAT, layer, publisher, subscriber,
-                        countAndSize.mCount);
-                writer.format(PACKET_FAILURE_SIZE_FORMAT, layer, publisher, subscriber,
-                        countAndSize.mSize);
-            }
-        }
     }
 
     /**
@@ -236,26 +169,6 @@
             mBrokerService.setPublisherLayersOffering(token, offering);
         }
 
-        private void incrementPacketCount(VmsLayer layer, long size) {
-            synchronized (mPacketCounts) {
-                PacketCountAndSize countAndSize = mPacketCounts.computeIfAbsent(layer,
-                        i -> new PacketCountAndSize());
-                countAndSize.mCount++;
-                countAndSize.mSize += size;
-            }
-        }
-
-        private void incrementPacketFailure(VmsLayer layer, String publisher, String subscriber,
-                long size) {
-            synchronized (mPacketFailureCounts) {
-                PacketFailureKey key = new PacketFailureKey(layer, publisher, subscriber);
-                PacketCountAndSize countAndSize = mPacketFailureCounts.computeIfAbsent(key,
-                        i -> new PacketCountAndSize());
-                countAndSize.mCount++;
-                countAndSize.mSize += size;
-            }
-        }
-
         @Override
         public void publish(IBinder token, VmsLayer layer, int publisherId, byte[] payload) {
             assertPermission(token);
@@ -268,7 +181,8 @@
             }
 
             int payloadLength = payload != null ? payload.length : 0;
-            incrementPacketCount(layer, payloadLength);
+            mStatsService.getVmsClientLog(mGetCallingUid.getAsInt())
+                    .logPacketSent(layer, payloadLength);
 
             // Send the message to subscribers
             Set<IVmsSubscriberClient> listeners =
@@ -277,17 +191,21 @@
             if (DBG) Log.d(TAG, String.format("Number of subscribers: %d", listeners.size()));
 
             if (listeners.size() == 0) {
-                // An empty string for the last argument is a special value signalizing zero
-                // subscribers for the VMS_PACKET_FAILURE_REPORTED atom.
-                incrementPacketFailure(layer, mName, "", payloadLength);
+                // A negative UID signals that the packet had zero subscribers
+                mStatsService.getVmsClientLog(-1)
+                        .logPacketDropped(layer, payloadLength);
             }
 
             for (IVmsSubscriberClient listener : listeners) {
+                int subscriberUid = mClientManager.getSubscriberUid(listener);
                 try {
                     listener.onVmsMessageReceived(layer, payload);
+                    mStatsService.getVmsClientLog(subscriberUid)
+                            .logPacketReceived(layer, payloadLength);
                 } catch (RemoteException ex) {
+                    mStatsService.getVmsClientLog(subscriberUid)
+                            .logPacketDropped(layer, payloadLength);
                     String subscriberName = mClientManager.getPackageName(listener);
-                    incrementPacketFailure(layer, mName, subscriberName, payloadLength);
                     Log.e(TAG, String.format("Unable to publish to listener: %s", subscriberName));
                 }
             }
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
new file mode 100644
index 0000000..d3e3c6f
--- /dev/null
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -0,0 +1,606 @@
+/*
+ * 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.am;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.Process.INVALID_UID;
+
+import static com.android.car.CarLog.TAG_AM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackInfo;
+import android.app.ActivityOptions;
+import android.app.IActivityManager;
+import android.app.IProcessObserver;
+import android.app.TaskStackListener;
+import android.car.hardware.power.CarPowerManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+
+import com.android.car.CarLocalServices;
+import com.android.car.CarServiceBase;
+import com.android.car.user.CarUserService;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Monitors top activity for a display and guarantee activity in fixed mode is re-launched if it has
+ * crashed or gone to background for whatever reason.
+ *
+ * <p>This component also monitors the upddate of the target package and re-launch it once
+ * update is complete.</p>
+ */
+public final class FixedActivityService implements CarServiceBase {
+
+    private static final boolean DBG = false;
+
+    private static final long RECHECK_INTERVAL_MS = 500;
+    private static final int MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY = 5;
+    // If process keep running without crashing, will reset consecutive crash counts.
+    private static final long CRASH_FORGET_INTERVAL_MS = 2 * 60 * 1000; // 2 mins
+
+    private static class RunningActivityInfo {
+        @NonNull
+        public final Intent intent;
+
+        @NonNull
+        public final ActivityOptions activityOptions;
+
+        @UserIdInt
+        public final int userId;
+
+        @GuardedBy("mLock")
+        public boolean isVisible;
+        @GuardedBy("mLock")
+        public long lastLaunchTimeMs = 0;
+        @GuardedBy("mLock")
+        public int consecutiveRetries = 0;
+        @GuardedBy("mLock")
+        public int taskId = INVALID_TASK_ID;
+        @GuardedBy("mLock")
+        public int previousTaskId = INVALID_TASK_ID;
+        @GuardedBy("mLock")
+        public boolean inBackground;
+        @GuardedBy("mLock")
+        public boolean failureLogged;
+
+        RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
+                @UserIdInt int userId) {
+            this.intent = intent;
+            this.activityOptions = activityOptions;
+            this.userId = userId;
+        }
+
+        private void resetCrashCounterLocked() {
+            consecutiveRetries = 0;
+            failureLogged = false;
+        }
+
+        @Override
+        public String toString() {
+            return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
+                    + ",userId:" + userId + ",isVisible:" + isVisible
+                    + ",lastLaunchTimeMs:" + lastLaunchTimeMs
+                    + ",consecutiveRetries:" + consecutiveRetries + ",taskId:" + taskId + "}";
+        }
+    }
+
+    private final Context mContext;
+
+    private final IActivityManager mAm;
+
+    private final UserManager mUm;
+
+    private final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
+        @Override
+        public void onUserLockChanged(@UserIdInt int userId, boolean unlocked) {
+            // Nothing to do
+        }
+
+        @Override
+        public void onSwitchUser(@UserIdInt int userId) {
+            synchronized (mLock) {
+                mRunningActivities.clear();
+            }
+        }
+    };
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+                    || Intent.ACTION_PACKAGE_REPLACED.equals(
+                    action)) {
+                Uri packageData = intent.getData();
+                if (packageData == null) {
+                    Log.w(TAG_AM, "null packageData");
+                    return;
+                }
+                String packageName = packageData.getSchemeSpecificPart();
+                if (packageName == null) {
+                    Log.w(TAG_AM, "null packageName");
+                    return;
+                }
+                int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                int userId = UserHandle.getUserId(uid);
+                boolean tryLaunch = false;
+                synchronized (mLock) {
+                    for (int i = 0; i < mRunningActivities.size(); i++) {
+                        RunningActivityInfo info = mRunningActivities.valueAt(i);
+                        ComponentName component = info.intent.getComponent();
+                        // should do this for all activities as the same package can cover multiple
+                        // displays.
+                        if (packageName.equals(component.getPackageName())
+                                && info.userId == userId) {
+                            Log.i(TAG_AM, "Package updated:" + packageName
+                                    + ",user:" + userId);
+                            info.resetCrashCounterLocked();
+                            tryLaunch = true;
+                        }
+                    }
+                }
+                if (tryLaunch) {
+                    launchIfNecessary();
+                }
+            }
+        }
+    };
+
+    // It says listener but is actually callback.
+    private final TaskStackListener mTaskStackListener = new TaskStackListener() {
+        @Override
+        public void onTaskStackChanged() {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskCreated(int taskId, ComponentName componentName) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskRemoved(int taskId) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskRemovalStarted(int taskId) {
+            launchIfNecessary();
+        }
+    };
+
+    private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
+        @Override
+        public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+          // ignore
+        }
+
+        @Override
+        public void onProcessDied(int pid, int uid) {
+            launchIfNecessary();
+        }
+    };
+
+    private final HandlerThread mHandlerThread = new HandlerThread(
+            FixedActivityService.class.getSimpleName());
+
+    private final Runnable mActivityCheckRunnable = () -> {
+        launchIfNecessary();
+    };
+
+    private final Object mLock = new Object();
+
+    // key: displayId
+    @GuardedBy("mLock")
+    private final SparseArray<RunningActivityInfo> mRunningActivities =
+            new SparseArray<>(/* capacity= */ 1); // default to one cluster only case
+
+    @GuardedBy("mLock")
+    private boolean mEventMonitoringActive;
+
+    @GuardedBy("mLock")
+    private CarPowerManager mCarPowerManager;
+
+    private final CarPowerManager.CarPowerStateListener mCarPowerStateListener = (state) -> {
+        if (state != CarPowerManager.CarPowerStateListener.ON) {
+            return;
+        }
+        synchronized (mLock) {
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                RunningActivityInfo info = mRunningActivities.valueAt(i);
+                info.resetCrashCounterLocked();
+            }
+        }
+        launchIfNecessary();
+    };
+
+    public FixedActivityService(Context context) {
+        mContext = context;
+        mAm = ActivityManager.getService();
+        mUm = context.getSystemService(UserManager.class);
+        mHandlerThread.start();
+    }
+
+    @Override
+    public void init() {
+        // nothing to do
+    }
+
+    @Override
+    public void release() {
+        stopMonitoringEvents();
+    }
+
+    @Override
+    public void dump(PrintWriter writer) {
+        writer.println("*FixedActivityService*");
+        synchronized (mLock) {
+            writer.println("mRunningActivities:" + mRunningActivities
+                    + " ,mEventMonitoringActive:" + mEventMonitoringActive);
+        }
+    }
+
+    private void postRecheck(long delayMs) {
+        mHandlerThread.getThreadHandler().postDelayed(mActivityCheckRunnable, delayMs);
+    }
+
+    private void startMonitoringEvents() {
+        CarPowerManager carPowerManager;
+        synchronized (mLock) {
+            if (mEventMonitoringActive) {
+                return;
+            }
+            mEventMonitoringActive = true;
+            carPowerManager = CarLocalServices.createCarPowerManager(mContext);
+            mCarPowerManager = carPowerManager;
+        }
+        CarUserService userService = CarLocalServices.getService(CarUserService.class);
+        userService.addUserCallback(mUserCallback);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        filter.addDataScheme("package");
+        mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
+                /* broadcastPermission= */ null, /* scheduler= */ null);
+        try {
+            mAm.registerTaskStackListener(mTaskStackListener);
+            mAm.registerProcessObserver(mProcessObserver);
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+        try {
+            carPowerManager.setListener(mCarPowerStateListener);
+        } catch (Exception e) {
+            // should not happen
+            Log.e(TAG_AM, "Got exception from CarPowerManager", e);
+        }
+    }
+
+    private void stopMonitoringEvents() {
+        CarPowerManager carPowerManager;
+        synchronized (mLock) {
+            if (!mEventMonitoringActive) {
+                return;
+            }
+            mEventMonitoringActive = false;
+            carPowerManager = mCarPowerManager;
+            mCarPowerManager = null;
+        }
+        if (carPowerManager != null) {
+            carPowerManager.clearListener();
+        }
+        mHandlerThread.getThreadHandler().removeCallbacks(mActivityCheckRunnable);
+        CarUserService userService = CarLocalServices.getService(CarUserService.class);
+        userService.removeUserCallback(mUserCallback);
+        try {
+            mAm.unregisterTaskStackListener(mTaskStackListener);
+            mAm.unregisterProcessObserver(mProcessObserver);
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    @Nullable
+    private List<StackInfo> getStackInfos() {
+        try {
+            return mAm.getAllStackInfos();
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+        return null;
+    }
+
+    /**
+     * Launches all stored fixed mode activities if necessary.
+     * @param displayId Display id to check if it is visible. If check is not necessary, should pass
+     *        {@link Display#INVALID_DISPLAY}.
+     * @return true if fixed Activity for given {@code displayId} is visible / successfully
+     *         launched. It will return false for {@link Display#INVALID_DISPLAY} {@code displayId}.
+     */
+    private boolean launchIfNecessary(int displayId) {
+        List<StackInfo> infos = getStackInfos();
+        if (infos == null) {
+            Log.e(TAG_AM, "cannot get StackInfo from AM");
+            return false;
+        }
+        long now = SystemClock.elapsedRealtime();
+        synchronized (mLock) {
+            if (mRunningActivities.size() == 0) {
+                // it must have been stopped.
+                if (DBG) {
+                    Log.i(TAG_AM, "empty activity list", new RuntimeException());
+                }
+                return false;
+            }
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                mRunningActivities.valueAt(i).isVisible = false;
+            }
+            for (StackInfo stackInfo : infos) {
+                RunningActivityInfo activityInfo = mRunningActivities.get(stackInfo.displayId);
+                if (activityInfo == null) {
+                    continue;
+                }
+                int topUserId = stackInfo.taskUserIds[stackInfo.taskUserIds.length - 1];
+                if (activityInfo.intent.getComponent().equals(stackInfo.topActivity)
+                        && activityInfo.userId == topUserId && stackInfo.visible) {
+                    // top one is matching.
+                    activityInfo.isVisible = true;
+                    activityInfo.taskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+                    continue;
+                }
+                activityInfo.previousTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+                Log.i(TAG_AM, "Unmatched top activity will be removed:"
+                        + stackInfo.topActivity + " top task id:" + activityInfo.previousTaskId
+                        + " user:" + topUserId + " display:" + stackInfo.displayId);
+                activityInfo.inBackground = false;
+                for (int i = 0; i < stackInfo.taskIds.length - 1; i++) {
+                    if (activityInfo.taskId == stackInfo.taskIds[i]) {
+                        activityInfo.inBackground = true;
+                    }
+                }
+                if (!activityInfo.inBackground) {
+                    activityInfo.taskId = INVALID_TASK_ID;
+                }
+            }
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
+                long timeSinceLastLaunchMs = now - activityInfo.lastLaunchTimeMs;
+                if (activityInfo.isVisible) {
+                    if (timeSinceLastLaunchMs >= CRASH_FORGET_INTERVAL_MS) {
+                        activityInfo.consecutiveRetries = 0;
+                    }
+                    continue;
+                }
+                if (!isComponentAvailable(activityInfo.intent.getComponent(),
+                        activityInfo.userId) || !isUserAllowedToLaunchActivity(
+                        activityInfo.userId)) {
+                    continue;
+                }
+                // For 1st call (consecutiveRetries == 0), do not wait as there can be no posting
+                // for recheck.
+                if (activityInfo.consecutiveRetries > 0 && (timeSinceLastLaunchMs
+                        < RECHECK_INTERVAL_MS)) {
+                    // wait until next check interval comes.
+                    continue;
+                }
+                if (activityInfo.consecutiveRetries >= MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY) {
+                    // re-tried too many times, give up for now.
+                    if (!activityInfo.failureLogged) {
+                        activityInfo.failureLogged = true;
+                        Log.w(TAG_AM, "Too many relaunch failure of fixed activity:"
+                                + activityInfo);
+                    }
+                    continue;
+                }
+
+                Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
+                        + ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
+                        + mRunningActivities.keyAt(i));
+                // Increase retry count if task is not in background. In case like other app is
+                // launched and the target activity is still in background, do not consider it
+                // as retry.
+                if (!activityInfo.inBackground) {
+                    activityInfo.consecutiveRetries++;
+                }
+                try {
+                    postRecheck(RECHECK_INTERVAL_MS);
+                    postRecheck(CRASH_FORGET_INTERVAL_MS);
+                    mContext.startActivityAsUser(activityInfo.intent,
+                            activityInfo.activityOptions.toBundle(),
+                            UserHandle.of(activityInfo.userId));
+                    activityInfo.isVisible = true;
+                    activityInfo.lastLaunchTimeMs = SystemClock.elapsedRealtime();
+                } catch (Exception e) { // Catch all for any app related issues.
+                    Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
+                }
+            }
+            RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+            if (activityInfo == null) {
+                return false;
+            }
+            return activityInfo.isVisible;
+        }
+    }
+
+    private void launchIfNecessary() {
+        launchIfNecessary(Display.INVALID_DISPLAY);
+    }
+
+    private void logComponentNotFound(ComponentName component, @UserIdInt  int userId,
+            Exception e) {
+        Log.e(TAG_AM, "Specified Component not found:" + component
+                + " for userid:" + userId, e);
+    }
+
+    private boolean isComponentAvailable(ComponentName component, @UserIdInt int userId) {
+        PackageInfo packageInfo;
+        try {
+            packageInfo = mContext.getPackageManager().getPackageInfoAsUser(
+                    component.getPackageName(), PackageManager.GET_ACTIVITIES, userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            logComponentNotFound(component, userId, e);
+            return false;
+        }
+        if (packageInfo == null || packageInfo.activities == null) {
+            // may not be necessary but additional safety check
+            logComponentNotFound(component, userId, new RuntimeException());
+            return false;
+        }
+        String fullName = component.getClassName();
+        String shortName = component.getShortClassName();
+        for (ActivityInfo info : packageInfo.activities) {
+            if (info.name.equals(fullName) || info.name.equals(shortName)) {
+                return true;
+            }
+        }
+        logComponentNotFound(component, userId, new RuntimeException());
+        return false;
+    }
+
+    private boolean isUserAllowedToLaunchActivity(@UserIdInt int userId) {
+        int currentUser = ActivityManager.getCurrentUser();
+        if (userId == currentUser) {
+            return true;
+        }
+        int[] profileIds = mUm.getEnabledProfileIds(currentUser);
+        for (int id : profileIds) {
+            if (id == userId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isDisplayAllowedForFixedMode(int displayId) {
+        if (displayId == Display.DEFAULT_DISPLAY || displayId == Display.INVALID_DISPLAY) {
+            Log.w(TAG_AM, "Target display cannot be used for fixed mode, displayId:" + displayId,
+                    new RuntimeException());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Checks {@link InstrumentClusterRenderingService#startFixedActivityModeForDisplayAndUser(
+     * Intent, ActivityOptions, int)}
+     */
+    public boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
+            @NonNull ActivityOptions options, int displayId, @UserIdInt int userId) {
+        if (!isDisplayAllowedForFixedMode(displayId)) {
+            return false;
+        }
+        if (options == null) {
+            Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, null options");
+            return false;
+        }
+        if (!isUserAllowedToLaunchActivity(userId)) {
+            Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
+                    + " cannot launch activity, Intent:" + intent);
+            return false;
+        }
+        ComponentName component = intent.getComponent();
+        if (component == null) {
+            Log.e(TAG_AM,
+                    "startFixedActivityModeForDisplayAndUser: No component specified for "
+                            + "requested Intent"
+                            + intent);
+            return false;
+        }
+        if (!isComponentAvailable(component, userId)) {
+            return false;
+        }
+        boolean startMonitoringEvents = false;
+        synchronized (mLock) {
+            if (mRunningActivities.size() == 0) {
+                startMonitoringEvents = true;
+            }
+            RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+            boolean replaceEntry = true;
+            if (activityInfo != null && activityInfo.intent.equals(intent)
+                    && options.equals(activityInfo.activityOptions)
+                    && userId == activityInfo.userId) {
+                replaceEntry = false;
+                if (activityInfo.isVisible) { // already shown.
+                    return true;
+                }
+            }
+            if (replaceEntry) {
+                activityInfo = new RunningActivityInfo(intent, options, userId);
+                mRunningActivities.put(displayId, activityInfo);
+            }
+        }
+        boolean launched = launchIfNecessary(displayId);
+        if (!launched) {
+            synchronized (mLock) {
+                mRunningActivities.remove(displayId);
+            }
+        }
+        // If first trial fails, let client know and do not retry as it can be wrong setting.
+        if (startMonitoringEvents && launched) {
+            startMonitoringEvents();
+        }
+        return launched;
+    }
+
+    /** Check {@link InstrumentClusterRenderingService#stopFixedActivityMode(int)} */
+    public void stopFixedActivityMode(int displayId) {
+        if (!isDisplayAllowedForFixedMode(displayId)) {
+            return;
+        }
+        boolean stopMonitoringEvents = false;
+        synchronized (mLock) {
+            mRunningActivities.remove(displayId);
+            if (mRunningActivities.size() == 0) {
+                stopMonitoringEvents = true;
+            }
+        }
+        if (stopMonitoringEvents) {
+            stopMonitoringEvents();
+        }
+    }
+}
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index fd16da5..cc0a6b7 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -15,17 +15,23 @@
  */
 package com.android.car.cluster;
 
+import static android.car.cluster.renderer.InstrumentClusterRenderingService.EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER;
+
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.ActivityOptions;
 import android.car.CarAppFocusManager;
 import android.car.cluster.IInstrumentClusterManagerCallback;
 import android.car.cluster.IInstrumentClusterManagerService;
 import android.car.cluster.renderer.IInstrumentCluster;
+import android.car.cluster.renderer.IInstrumentClusterHelper;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -43,6 +49,7 @@
 import com.android.car.CarLog;
 import com.android.car.CarServiceBase;
 import com.android.car.R;
+import com.android.car.am.FixedActivityService;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 
@@ -69,16 +76,18 @@
      */
     @Deprecated
     private final ClusterManagerService mClusterManagerService = new ClusterManagerService();
-    private final Object mSync = new Object();
-    @GuardedBy("mSync")
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
     private ContextOwner mNavContextOwner = NO_OWNER;
-    @GuardedBy("mSync")
+    @GuardedBy("mLock")
     private IInstrumentCluster mRendererService;
     // If renderer service crashed / stopped and this class fails to rebind with it immediately,
     // we should wait some time before next attempt. This may happen during APK update for example.
+    @GuardedBy("mLock")
     private DeferredRebinder mDeferredRebinder;
     // Whether {@link android.car.cluster.renderer.InstrumentClusterRendererService} is bound
     // (although not necessarily connected)
+    @GuardedBy("mLock")
     private boolean mRendererBound = false;
 
     /**
@@ -92,7 +101,7 @@
             }
             IInstrumentCluster service = IInstrumentCluster.Stub.asInterface(binder);
             ContextOwner navContextOwner;
-            synchronized (mSync) {
+            synchronized (mLock) {
                 mRendererService = service;
                 navContextOwner = mNavContextOwner;
             }
@@ -107,19 +116,41 @@
                 Log.d(TAG, "onServiceDisconnected, name: " + name);
             }
             mContext.unbindService(this);
-            mRendererBound = false;
-
-            synchronized (mSync) {
+            DeferredRebinder rebinder;
+            synchronized (mLock) {
+                mRendererBound = false;
                 mRendererService = null;
+                if (mDeferredRebinder == null) {
+                    mDeferredRebinder = new DeferredRebinder();
+                }
+                rebinder = mDeferredRebinder;
             }
-
-            if (mDeferredRebinder == null) {
-                mDeferredRebinder = new DeferredRebinder();
-            }
-            mDeferredRebinder.rebind();
+            rebinder.rebind();
         }
     };
 
+    private final IInstrumentClusterHelper mInstrumentClusterHelper =
+            new IInstrumentClusterHelper.Stub() {
+                @Override
+                public boolean startFixedActivityModeForDisplayAndUser(Intent intent,
+                        Bundle activityOptionsBundle, int userId) {
+                    Binder.clearCallingIdentity();
+                    ActivityOptions options = new ActivityOptions(activityOptionsBundle);
+                    FixedActivityService service = CarLocalServices.getService(
+                            FixedActivityService.class);
+                    return service.startFixedActivityModeForDisplayAndUser(intent, options,
+                            options.getLaunchDisplayId(), userId);
+                }
+
+                @Override
+                public void stopFixedActivityMode(int displayId) {
+                    Binder.clearCallingIdentity();
+                    FixedActivityService service = CarLocalServices.getService(
+                            FixedActivityService.class);
+                    service.stopFixedActivityMode(displayId);
+                }
+            };
+
     public InstrumentClusterService(Context context, AppFocusService appFocusService,
             CarInputService carInputService) {
         mContext = context;
@@ -181,7 +212,7 @@
         IInstrumentCluster service;
         ContextOwner requester = new ContextOwner(uid, pid);
         ContextOwner newOwner = acquire ? requester : NO_OWNER;
-        synchronized (mSync) {
+        synchronized (mLock) {
             if ((acquire && Objects.equals(mNavContextOwner, requester))
                     || (!acquire && !Objects.equals(mNavContextOwner, requester))) {
                 // Nothing to do here. Either the same owner is acquiring twice, or someone is
@@ -221,6 +252,11 @@
 
         Intent intent = new Intent();
         intent.setComponent(ComponentName.unflattenFromString(rendererService));
+        // Litle bit inefficiency here as Intent.getIBinderExtra() is a hidden API.
+        Bundle bundle = new Bundle();
+        bundle.putBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER,
+                mInstrumentClusterHelper.asBinder());
+        intent.putExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER, bundle);
         return mContext.bindServiceAsUser(intent, mRendererServiceConnection,
                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM);
     }
@@ -262,7 +298,7 @@
 
     private IInstrumentCluster getInstrumentClusterRendererService() {
         IInstrumentCluster service;
-        synchronized (mSync) {
+        synchronized (mLock) {
             service = mRendererService;
         }
         return service;
diff --git a/service/src/com/android/car/hal/PropertyHalService.java b/service/src/com/android/car/hal/PropertyHalService.java
index 545fc2b..484e667 100644
--- a/service/src/com/android/car/hal/PropertyHalService.java
+++ b/service/src/com/android/car/hal/PropertyHalService.java
@@ -183,6 +183,14 @@
     }
 
     /**
+     * Return true if property is a display_units property
+     * @param propId
+     */
+    public boolean isDisplayUnitsProperty(int propId) {
+        return mPropIds.isPropertyToChangeUnits(propId);
+    }
+
+    /**
      * Set the property value.
      * @param prop
      */
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index 5409b4d..82a89d7 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -26,6 +26,8 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import java.util.HashSet;
+
 /**
  * Helper class to define which property IDs are used by PropertyHalService.  This class binds the
  * read and write permissions to the property ID.
@@ -39,11 +41,12 @@
      * properties.
      */
     private final SparseArray<Pair<String, String>> mProps;
+    private final HashSet<Integer> mPropForUnits;
     private static final String TAG = "PropertyHalServiceIds";
 
     public PropertyHalServiceIds() {
         mProps = new SparseArray<>();
-
+        mPropForUnits = new HashSet<>();
         // Add propertyId and read/write permissions
         // Cabin Properties
         mProps.put(VehicleProperty.DOOR_POS, new Pair<>(
@@ -385,24 +388,31 @@
         mProps.put(VehicleProperty.CABIN_LIGHTS_SWITCH, new Pair<>(
                 Car.PERMISSION_CONTROL_INTERIOR_LIGHTS,
                 Car.PERMISSION_CONTROL_INTERIOR_LIGHTS));
+        // Display_Units
         mProps.put(VehicleProperty.DISTANCE_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.DISTANCE_DISPLAY_UNITS);
         mProps.put(VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS);
         mProps.put(VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS);
         mProps.put(VehicleProperty.EV_BATTERY_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.EV_BATTERY_DISPLAY_UNITS);
         mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME);
         mProps.put(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mPropForUnits.add(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
     }
 
     /**
@@ -469,4 +479,11 @@
             return insertVendorProperty(propId);
         }
     }
+
+    /**
+     * Check if the property is one of display units properties.
+     */
+    public boolean isPropertyToChangeUnits(int propertyId) {
+        return mPropForUnits.contains(propertyId);
+    }
 }
diff --git a/service/src/com/android/car/hal/VmsHalService.java b/service/src/com/android/car/hal/VmsHalService.java
index bba5d5f..99263d7 100644
--- a/service/src/com/android/car/hal/VmsHalService.java
+++ b/service/src/com/android/car/hal/VmsHalService.java
@@ -17,8 +17,6 @@
 
 import static com.android.car.CarServiceUtils.toByteArray;
 
-import static java.lang.Integer.toHexString;
-
 import android.car.VehicleAreaType;
 import android.car.vms.IVmsPublisherClient;
 import android.car.vms.IVmsPublisherService;
@@ -43,6 +41,7 @@
 import android.hardware.automotive.vehicle.V2_0.VmsOfferingMessageIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsPublisherInformationIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsStartSessionMessageIntegerValuesIndex;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -54,7 +53,6 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.car.CarLog;
 import com.android.car.vms.VmsClientManager;
 
 import java.io.FileDescriptor;
@@ -87,6 +85,7 @@
     private final int mCoreId;
     private final MessageQueue mMessageQueue;
     private final int mClientMetricsProperty;
+    private final boolean mPropagatePropertyException;
     private volatile boolean mIsSupported = false;
 
     private VmsClientManager mClientManager;
@@ -186,11 +185,7 @@
             int messageType = msg.what;
             VehiclePropValue vehicleProp = (VehiclePropValue) msg.obj;
             if (DBG) Log.d(TAG, "Sending " + VmsMessageType.toString(messageType) + " message");
-            try {
-                setPropertyValue(vehicleProp);
-            } catch (RemoteException e) {
-                Log.e(TAG, "While sending " + VmsMessageType.toString(messageType));
-            }
+            setPropertyValue(vehicleProp);
             return true;
         }
     }
@@ -199,15 +194,17 @@
      * Constructor used by {@link VehicleHal}
      */
     VmsHalService(Context context, VehicleHal vehicleHal) {
-        this(context, vehicleHal, SystemClock::uptimeMillis);
+        this(context, vehicleHal, SystemClock::uptimeMillis, (Build.IS_ENG || Build.IS_USERDEBUG));
     }
 
     @VisibleForTesting
-    VmsHalService(Context context, VehicleHal vehicleHal, Supplier<Long> getCoreId) {
+    VmsHalService(Context context, VehicleHal vehicleHal, Supplier<Long> getCoreId,
+            boolean propagatePropertyException) {
         mVehicleHal = vehicleHal;
         mCoreId = (int) (getCoreId.get() % Integer.MAX_VALUE);
         mMessageQueue = new MessageQueue();
         mClientMetricsProperty = getClientMetricsProperty(context);
+        mPropagatePropertyException = propagatePropertyException;
     }
 
     private static int getClientMetricsProperty(Context context) {
@@ -326,8 +323,9 @@
         VehiclePropValue vehicleProp = null;
         try {
             vehicleProp = mVehicleHal.get(mClientMetricsProperty);
-        } catch (PropertyTimeoutException e) {
-            Log.e(TAG, "Timeout while reading metrics from client");
+        } catch (PropertyTimeoutException | RuntimeException e) {
+            // Failures to retrieve metrics should be non-fatal
+            Log.e(TAG, "While reading metrics from client", e);
         }
         if (vehicleProp == null) {
             if (DBG) Log.d(TAG, "Metrics unavailable");
@@ -395,7 +393,7 @@
                         Log.e(TAG, "Unexpected message type: " + messageType);
                 }
             } catch (IndexOutOfBoundsException | RemoteException e) {
-                Log.e(TAG, "While handling: " + messageType, e);
+                Log.e(TAG, "While handling " + VmsMessageType.toString(messageType), e);
             }
         }
     }
@@ -425,9 +423,8 @@
             mSubscriptionStateSequence = -1;
             mAvailableLayersSequence = -1;
 
-            // Enqueue an acknowledgement message
-            mMessageQueue.enqueue(VmsMessageType.START_SESSION,
-                    createStartSessionMessage(mCoreId, clientId));
+            // Send acknowledgement message
+            setPropertyValue(createStartSessionMessage(mCoreId, clientId));
         }
 
         // Notify client manager of connection
@@ -678,7 +675,7 @@
                         mPublisherService.getSubscriptions()));
     }
 
-    private void setPropertyValue(VehiclePropValue vehicleProp) throws RemoteException {
+    private void setPropertyValue(VehiclePropValue vehicleProp) {
         int messageType = vehicleProp.value.int32Values.get(
                 VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
 
@@ -690,11 +687,11 @@
 
         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));
+        } catch (PropertyTimeoutException | RuntimeException e) {
+            Log.e(TAG, "While sending " + VmsMessageType.toString(messageType), e.getCause());
+            if (mPropagatePropertyException) {
+                throw new IllegalStateException(e);
+            }
         }
     }
 
diff --git a/service/src/com/android/car/pm/ActivityBlockingActivity.java b/service/src/com/android/car/pm/ActivityBlockingActivity.java
index 9dcb70a..9756523 100644
--- a/service/src/com/android/car/pm/ActivityBlockingActivity.java
+++ b/service/src/com/android/car/pm/ActivityBlockingActivity.java
@@ -79,14 +79,19 @@
         // restrictions are lifted.
         // This Activity should be launched only after car service is initialized. Currently this
         // Activity is only launched from CPMS. So this is safe to do.
-        mCar = Car.createCar(this);
-        mUxRManager = (CarUxRestrictionsManager) mCar.getCarManager(
-                Car.CAR_UX_RESTRICTION_SERVICE);
-        // This activity would have been launched only in a restricted state.
-        // But ensuring when the service connection is established, that we are still
-        // in a restricted state.
-        handleUxRChange(mUxRManager.getCurrentCarUxRestrictions());
-        mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange);
+        mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+                (car, ready) -> {
+                    if (!ready) {
+                        return;
+                    }
+                    mUxRManager = (CarUxRestrictionsManager) car.getCarManager(
+                            Car.CAR_UX_RESTRICTION_SERVICE);
+                    // This activity would have been launched only in a restricted state.
+                    // But ensuring when the service connection is established, that we are still
+                    // in a restricted state.
+                    handleUxRChange(mUxRManager.getCurrentCarUxRestrictions());
+                    mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange);
+                });
     }
 
     @Override
diff --git a/service/src/com/android/car/stats/CarStatsService.java b/service/src/com/android/car/stats/CarStatsService.java
new file mode 100644
index 0000000..64fbf1f
--- /dev/null
+++ b/service/src/com/android/car/stats/CarStatsService.java
@@ -0,0 +1,181 @@
+/*
+ * 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.stats;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.StatsLogEventWrapper;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.StatsLog;
+
+import com.android.car.stats.VmsClientLog.ConnectionState;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.car.ICarStatsService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Implementation of {@link ICarStatsService}, for reporting pulled atoms via statsd.
+ *
+ * Also implements collection and dumpsys reporting of atoms in CSV format.
+ */
+public class CarStatsService extends ICarStatsService.Stub {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "CarStatsService";
+    private static final String VMS_CONNECTION_STATS_DUMPSYS_HEADER =
+            "uid,packageName,attempts,connected,disconnected,terminated,errors";
+
+    private static final Function<VmsClientLog, String> VMS_CONNECTION_STATS_DUMPSYS_FORMAT =
+            entry -> String.format(Locale.US,
+                    "%d,%s,%d,%d,%d,%d,%d",
+                    entry.getUid(), entry.getPackageName(),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTING),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTED),
+                    entry.getConnectionStateCount(ConnectionState.DISCONNECTED),
+                    entry.getConnectionStateCount(ConnectionState.TERMINATED),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTION_ERROR));
+
+    private static final String VMS_CLIENT_STATS_DUMPSYS_HEADER =
+            "uid,layerType,layerChannel,layerVersion,"
+                    + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets";
+
+    private static final Function<VmsClientStats, String> VMS_CLIENT_STATS_DUMPSYS_FORMAT =
+            entry -> String.format(
+                    "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
+                    entry.getUid(),
+                    entry.getLayerType(), entry.getLayerChannel(), entry.getLayerVersion(),
+                    entry.getTxBytes(), entry.getTxPackets(),
+                    entry.getRxBytes(), entry.getRxPackets(),
+                    entry.getDroppedBytes(), entry.getDroppedPackets());
+
+    private static final Comparator<VmsClientStats> VMS_CLIENT_STATS_ORDER =
+            Comparator.comparingInt(VmsClientStats::getUid)
+                    .thenComparingInt(VmsClientStats::getLayerType)
+                    .thenComparingInt(VmsClientStats::getLayerChannel)
+                    .thenComparingInt(VmsClientStats::getLayerVersion);
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+
+    @GuardedBy("mVmsClientStats")
+    private final Map<Integer, VmsClientLog> mVmsClientStats = new ArrayMap<>();
+
+    public CarStatsService(Context context) {
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+    }
+
+    /**
+     * Gets a logger for the VMS client with a given UID.
+     */
+    public VmsClientLog getVmsClientLog(int clientUid) {
+        synchronized (mVmsClientStats) {
+            return mVmsClientStats.computeIfAbsent(
+                    clientUid,
+                    uid -> {
+                        String packageName = mPackageManager.getNameForUid(uid);
+                        if (DEBUG) {
+                            Log.d(TAG, "Created VmsClientLog: " + packageName);
+                        }
+                        return new VmsClientLog(uid, packageName);
+                    });
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        List<String> flags = Arrays.asList(args);
+        if (args.length == 0 || flags.contains("--vms-client")) {
+            dumpVmsStats(writer);
+        }
+    }
+
+    @Override
+    public StatsLogEventWrapper[] pullData(int tagId) {
+        mContext.enforceCallingPermission(Manifest.permission.DUMP, null);
+        if (tagId != StatsLog.VMS_CLIENT_STATS) {
+            Log.w(TAG, "Unexpected tagId: " + tagId);
+            return null;
+        }
+
+        List<StatsLogEventWrapper> ret = new ArrayList<>();
+        long elapsedNanos = SystemClock.elapsedRealtimeNanos();
+        long wallClockNanos = SystemClock.currentTimeMicro() * 1000L;
+        pullVmsClientStats(tagId, elapsedNanos, wallClockNanos, ret);
+        return ret.toArray(new StatsLogEventWrapper[0]);
+    }
+
+    private void dumpVmsStats(PrintWriter writer) {
+        synchronized (mVmsClientStats) {
+            writer.println(VMS_CONNECTION_STATS_DUMPSYS_HEADER);
+            mVmsClientStats.values().stream()
+                    // Unknown UID will not have connection stats
+                    .filter(entry -> entry.getUid() > 0)
+                    // Sort stats by UID
+                    .sorted(Comparator.comparingInt(VmsClientLog::getUid))
+                    .forEachOrdered(entry -> writer.println(
+                            VMS_CONNECTION_STATS_DUMPSYS_FORMAT.apply(entry)));
+            writer.println();
+
+            writer.println(VMS_CLIENT_STATS_DUMPSYS_HEADER);
+            dumpVmsClientStats(entry -> writer.println(
+                    VMS_CLIENT_STATS_DUMPSYS_FORMAT.apply(entry)));
+        }
+    }
+
+    private void pullVmsClientStats(int tagId, long elapsedNanos, long wallClockNanos,
+            List<StatsLogEventWrapper> pulledData) {
+        dumpVmsClientStats((entry) -> {
+            StatsLogEventWrapper e =
+                    new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos);
+            e.writeInt(entry.getUid());
+
+            e.writeInt(entry.getLayerType());
+            e.writeInt(entry.getLayerChannel());
+            e.writeInt(entry.getLayerVersion());
+
+            e.writeLong(entry.getTxBytes());
+            e.writeLong(entry.getTxPackets());
+            e.writeLong(entry.getRxBytes());
+            e.writeLong(entry.getRxPackets());
+            e.writeLong(entry.getDroppedBytes());
+            e.writeLong(entry.getDroppedPackets());
+            pulledData.add(e);
+        });
+    }
+
+    private void dumpVmsClientStats(Consumer<VmsClientStats> dumpFn) {
+        synchronized (mVmsClientStats) {
+            mVmsClientStats.values().stream()
+                    .flatMap(log -> log.getLayerEntries().stream())
+                    .sorted(VMS_CLIENT_STATS_ORDER)
+                    .forEachOrdered(dumpFn);
+        }
+    }
+}
diff --git a/service/src/com/android/car/stats/VmsClientLog.java b/service/src/com/android/car/stats/VmsClientLog.java
new file mode 100644
index 0000000..0fa2198
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientLog.java
@@ -0,0 +1,150 @@
+/*
+ * 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.stats;
+
+import android.annotation.Nullable;
+import android.car.vms.VmsLayer;
+import android.util.ArrayMap;
+import android.util.StatsLog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * Logger for per-client VMS statistics.
+ */
+public class VmsClientLog {
+    /**
+     * Constants used for identifying client connection states.
+     */
+    public static class ConnectionState {
+        // Attempting to connect to the client
+        public static final int CONNECTING =
+                StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTING;
+        // Client connection established
+        public static final int CONNECTED =
+                StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTED;
+        // Client connection closed unexpectedly
+        public static final int DISCONNECTED =
+                StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__DISCONNECTED;
+        // Client connection closed by VMS
+        public static final int TERMINATED =
+                StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__TERMINATED;
+        // Error establishing the client connection
+        public static final int CONNECTION_ERROR =
+                StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTION_ERROR;
+    }
+
+    private final Object mLock = new Object();
+
+    private final int mUid;
+    private final String mPackageName;
+
+    @GuardedBy("mLock")
+    private Map<Integer, AtomicLong> mConnectionStateCounters = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    private final Map<VmsLayer, VmsClientStats> mLayerStats = new ArrayMap<>();
+
+    VmsClientLog(int clientUid, @Nullable String clientPackage) {
+        mUid = clientUid;
+        mPackageName = clientPackage != null ? clientPackage : "";
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Logs a connection state change for the client.
+     *
+     * @param connectionState New connection state
+     */
+    public void logConnectionState(int connectionState) {
+        StatsLog.write(StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED,
+                mUid, mPackageName, connectionState);
+
+        AtomicLong counter;
+        synchronized (mLock) {
+            counter = mConnectionStateCounters.computeIfAbsent(connectionState,
+                    ignored -> new AtomicLong());
+        }
+        counter.incrementAndGet();
+    }
+
+    long getConnectionStateCount(int connectionState) {
+        AtomicLong counter;
+        synchronized (mLock) {
+            counter = mConnectionStateCounters.get(connectionState);
+        }
+        return counter == null ? 0L : counter.get();
+    }
+
+    /**
+     * Logs that a packet was published by the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketSent(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetSent(size);
+    }
+
+    /**
+     * Logs that a packet was received successfully by the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketReceived(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetReceived(size);
+    }
+
+    /**
+     * Logs that a packet was dropped due to an error delivering to the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketDropped(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetDropped(size);
+    }
+
+    Collection<VmsClientStats> getLayerEntries() {
+        synchronized (mLock) {
+            return mLayerStats.values().stream()
+                    .map(VmsClientStats::new) // Make a deep copy of the entries
+                    .collect(Collectors.toList());
+        }
+    }
+
+    private VmsClientStats getLayerEntry(VmsLayer layer) {
+        synchronized (mLock) {
+            return mLayerStats.computeIfAbsent(
+                    layer,
+                    (k) -> new VmsClientStats(mUid, layer));
+        }
+    }
+}
diff --git a/service/src/com/android/car/stats/VmsClientStats.java b/service/src/com/android/car/stats/VmsClientStats.java
new file mode 100644
index 0000000..f4ce7b8
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientStats.java
@@ -0,0 +1,174 @@
+/*
+ * 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.stats;
+
+import android.car.vms.VmsLayer;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Java representation of VmsClientStats statsd atom.
+ *
+ * All access to this class is synchronized through VmsClientLog.
+ */
+class VmsClientStats {
+    private final Object mLock = new Object();
+
+    private final int mUid;
+
+    private final int mLayerType;
+    private final int mLayerChannel;
+    private final int mLayerVersion;
+
+    @GuardedBy("mLock")
+    private long mTxBytes;
+    @GuardedBy("mLock")
+    private long mTxPackets;
+
+    @GuardedBy("mLock")
+    private long mRxBytes;
+    @GuardedBy("mLock")
+    private long mRxPackets;
+
+    @GuardedBy("mLock")
+    private long mDroppedBytes;
+    @GuardedBy("mLock")
+    private long mDroppedPackets;
+
+    /**
+     * Constructor for a VmsClientStats entry.
+     *
+     * @param uid UID of client package.
+     * @param layer Vehicle Maps Service layer.
+     */
+    VmsClientStats(int uid, VmsLayer layer) {
+        this.mUid = uid;
+
+        this.mLayerType = layer.getType();
+        this.mLayerChannel = layer.getSubtype();
+        this.mLayerVersion = layer.getVersion();
+    }
+
+    /**
+     * Copy constructor for entries exported from {@link VmsClientLog} to {@link CarStatsService}.
+     */
+    VmsClientStats(VmsClientStats other) {
+        synchronized (other.mLock) {
+            this.mUid = other.mUid;
+
+            this.mLayerType = other.mLayerType;
+            this.mLayerChannel = other.mLayerChannel;
+            this.mLayerVersion = other.mLayerVersion;
+
+            this.mTxBytes = other.mTxBytes;
+            this.mTxPackets = other.mTxPackets;
+            this.mRxBytes = other.mRxBytes;
+            this.mRxPackets = other.mRxPackets;
+            this.mDroppedBytes = other.mDroppedBytes;
+            this.mDroppedPackets = other.mDroppedPackets;
+        }
+    }
+
+    /**
+     * Records that a packet was sent by a publisher client.
+     *
+     * @param size Size of packet.
+     */
+    void packetSent(long size) {
+        synchronized (mLock) {
+            mTxBytes += size;
+            mTxPackets++;
+        }
+    }
+
+    /**
+     * Records that a packet was successfully received by a subscriber client.
+     *
+     * @param size Size of packet.
+     */
+    void packetReceived(long size) {
+        synchronized (mLock) {
+            mRxBytes += size;
+            mRxPackets++;
+        }
+    }
+
+    /**
+     * Records that a packet was dropped while being delivered to a subscriber client.
+     *
+     * @param size Size of packet.
+     */
+    void packetDropped(long size) {
+        synchronized (mLock) {
+            mDroppedBytes += size;
+            mDroppedPackets++;
+        }
+    }
+
+    int getUid() {
+        return mUid;
+    }
+
+    int getLayerType() {
+        return mLayerType;
+    }
+
+    int getLayerChannel() {
+        return mLayerChannel;
+    }
+
+    int getLayerVersion() {
+        return mLayerVersion;
+    }
+
+    long getTxBytes() {
+        synchronized (mLock) {
+            return mTxBytes;
+        }
+    }
+
+    long getTxPackets() {
+        synchronized (mLock) {
+            return mTxPackets;
+        }
+    }
+
+    long getRxBytes() {
+        synchronized (mLock) {
+            return mRxBytes;
+        }
+    }
+
+    long getRxPackets() {
+        synchronized (mLock) {
+            return mRxPackets;
+        }
+    }
+
+    long getDroppedBytes() {
+        synchronized (mLock) {
+            return mDroppedBytes;
+        }
+    }
+
+    long getDroppedPackets() {
+        synchronized (mLock) {
+            return mDroppedPackets;
+        }
+    }
+}
+
diff --git a/service/src/com/android/car/systeminterface/TimeInterface.java b/service/src/com/android/car/systeminterface/TimeInterface.java
index dea1153..fd350a5 100644
--- a/service/src/com/android/car/systeminterface/TimeInterface.java
+++ b/service/src/com/android/car/systeminterface/TimeInterface.java
@@ -19,6 +19,9 @@
 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 
 import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
@@ -42,16 +45,34 @@
     void cancelAllActions();
 
     class DefaultImpl implements TimeInterface {
-        private final ScheduledExecutorService mExecutor = newSingleThreadScheduledExecutor();
+        private final Object mLock = new Object();
+
+        @GuardedBy("mLock")
+        private ScheduledExecutorService mExecutor;
 
         @Override
         public void scheduleAction(Runnable r, long delayMs) {
-            mExecutor.scheduleAtFixedRate(r, delayMs, delayMs, TimeUnit.MILLISECONDS);
+            ScheduledExecutorService executor;
+            synchronized (mLock) {
+                executor = mExecutor;
+                if (executor == null) {
+                    executor = newSingleThreadScheduledExecutor();
+                    mExecutor = executor;
+                }
+            }
+            executor.scheduleAtFixedRate(r, delayMs, delayMs, TimeUnit.MILLISECONDS);
         }
 
         @Override
         public void cancelAllActions() {
-            mExecutor.shutdownNow();
+            ScheduledExecutorService executor;
+            synchronized (mLock) {
+                executor = mExecutor;
+                mExecutor = null;
+            }
+            if (executor != null) {
+                executor.shutdownNow();
+            }
         }
     }
 }
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 0f54647..7f2923d 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -16,6 +16,7 @@
 
 package com.android.car.trust;
 
+import static android.car.Car.PERMISSION_CAR_ENROLL_TRUST;
 import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_HANDSHAKE_FAILURE;
 import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_NOT_ALLOWED;
 
@@ -33,6 +34,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.bluetooth.BluetoothDevice;
 import android.car.encryptionrunner.EncryptionRunner;
@@ -52,6 +54,7 @@
 import android.util.Log;
 
 import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
+import com.android.car.ICarImpl;
 import com.android.car.R;
 import com.android.car.Utils;
 import com.android.internal.annotations.GuardedBy;
@@ -171,7 +174,9 @@
      * the enrollment of the trusted device.
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void startEnrollmentAdvertising() {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (!mTrustedDeviceService.getSharedPrefs()
                 .getBoolean(TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY, true)) {
             Log.e(TAG, "Trusted Device Enrollment disabled");
@@ -192,7 +197,9 @@
      * Stop BLE advertisement for Enrollment
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void stopEnrollmentAdvertising() {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         logEnrollmentEvent(STOP_ENROLLMENT_ADVERTISING);
         addEnrollmentServiceLog("stopEnrollmentAdvertising");
         mCarTrustAgentBleManager.stopEnrollmentAdvertising();
@@ -205,7 +212,9 @@
      * @param device the remote Bluetooth device that will receive the signal.
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void enrollmentHandshakeAccepted(BluetoothDevice device) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         logEnrollmentEvent(ENROLLMENT_HANDSHAKE_ACCEPTED);
         addEnrollmentServiceLog("enrollmentHandshakeAccepted");
         if (device == null || !device.equals(mRemoteEnrollmentDevice)) {
@@ -226,7 +235,9 @@
      * navigated away from the app before completing enrollment.
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void terminateEnrollmentHandshake() {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         addEnrollmentServiceLog("terminateEnrollmentHandshake");
         // Disconnect from BLE
         mCarTrustAgentBleManager.disconnectRemoteDevice();
@@ -252,7 +263,9 @@
      * @return True if the escrow token is active, false if not
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public boolean isEscrowTokenActive(long handle, int uid) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (mTokenActiveStateMap.get(handle) != null) {
             return mTokenActiveStateMap.get(handle);
         }
@@ -266,7 +279,9 @@
      * @param uid    user id
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void removeEscrowToken(long handle, int uid) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         mEnrollmentDelegate.removeEscrowToken(handle, uid);
         addEnrollmentServiceLog("removeEscrowToken (handle:" + handle + " uid:" + uid + ")");
     }
@@ -277,7 +292,9 @@
      * @param uid user id
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void removeAllTrustedDevices(int uid) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         for (TrustedDeviceInfo device : getEnrolledDeviceInfosForUser(uid)) {
             removeEscrowToken(device.getHandle(), uid);
         }
@@ -291,7 +308,9 @@
      * @param isEnabled {@code true} to enable; {@code false} to disable the feature.
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void setTrustedDeviceEnrollmentEnabled(boolean isEnabled) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         SharedPreferences.Editor editor = mTrustedDeviceService.getSharedPrefs().edit();
         editor.putBoolean(TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY, isEnabled);
         if (!editor.commit()) {
@@ -308,7 +327,9 @@
      *                  back.
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public void setTrustedDeviceUnlockEnabled(boolean isEnabled) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         mTrustedDeviceService.getCarTrustAgentUnlockService()
                 .setTrustedDeviceUnlockEnabled(isEnabled);
     }
@@ -322,7 +343,9 @@
      */
     @NonNull
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public List<TrustedDeviceInfo> getEnrolledDeviceInfosForUser(int uid) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         Set<String> enrolledDeviceInfos = mTrustedDeviceService.getSharedPrefs().getStringSet(
                 String.valueOf(uid), new HashSet<>());
         List<TrustedDeviceInfo> trustedDeviceInfos = new ArrayList<>(enrolledDeviceInfos.size());
@@ -342,7 +365,9 @@
      * @param listener {@link ICarTrustAgentEnrollmentCallback}
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public synchronized void registerEnrollmentCallback(ICarTrustAgentEnrollmentCallback listener) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (listener == null) {
             throw new IllegalArgumentException("Listener is null");
         }
@@ -835,8 +860,10 @@
      * @param listener client to unregister
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public synchronized void unregisterEnrollmentCallback(
             ICarTrustAgentEnrollmentCallback listener) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (listener == null) {
             throw new IllegalArgumentException("Listener is null");
         }
@@ -858,7 +885,9 @@
      * @param listener {@link ICarTrustAgentBleCallback}
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public synchronized void registerBleCallback(ICarTrustAgentBleCallback listener) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (listener == null) {
             throw new IllegalArgumentException("Listener is null");
         }
@@ -903,7 +932,9 @@
      * @param listener client to unregister
      */
     @Override
+    @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
     public synchronized void unregisterBleCallback(ICarTrustAgentBleCallback listener) {
+        ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
         if (listener == null) {
             throw new IllegalArgumentException("Listener is null");
         }
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 710793c..346c730 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -17,14 +17,11 @@
 package com.android.car.vms;
 
 import android.car.Car;
-import android.car.userlib.CarUserManagerHelper;
 import android.car.vms.IVmsPublisherClient;
 import android.car.vms.IVmsSubscriberClient;
-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.pm.ServiceInfo;
@@ -32,6 +29,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -42,16 +40,17 @@
 import com.android.car.R;
 import com.android.car.VmsPublisherService;
 import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
+import com.android.car.stats.VmsClientLog.ConnectionState;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.IntSupplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -70,12 +69,12 @@
 
     private final Context mContext;
     private final PackageManager mPackageManager;
-    private final Handler mHandler;
     private final UserManager mUserManager;
     private final CarUserService mUserService;
-    private final CarUserManagerHelper mUserManagerHelper;
-    private final int mMillisBeforeRebind;
+    private final CarStatsService mStatsService;
+    private final Handler mHandler;
     private final IntSupplier mGetCallingUid;
+    private final int mMillisBeforeRebind;
 
     private final Object mLock = new Object();
 
@@ -97,10 +96,7 @@
     private int mCurrentUser;
 
     @GuardedBy("mLock")
-    private final Map<IBinder, SubscriberConnection> mSubscribers = new HashMap<>();
-
-    @GuardedBy("mRebindCounts")
-    private final Map<String, AtomicLong> mRebindCounts = new ArrayMap<>();
+    private final Map<IBinder, SubscriberConnection> mSubscribers = new ArrayMap<>();
 
     @VisibleForTesting
     final Runnable mSystemUserUnlockedListener = () -> {
@@ -111,22 +107,25 @@
     };
 
     @VisibleForTesting
-    final BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
+    public final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
         @Override
-        public void onReceive(Context context, Intent intent) {
-            if (DBG) Log.d(TAG, "Received " + intent);
+        public void onSwitchUser(int userId) {
             synchronized (mLock) {
-                int currentUserId = mUserManagerHelper.getCurrentForegroundUserId();
-                if (mCurrentUser != currentUserId) {
+                if (mCurrentUser != userId) {
+                    mCurrentUser = userId;
                     terminate(mCurrentUserClients);
                     terminate(mSubscribers.values().stream()
-                            .filter(subscriber -> subscriber.mUserId != currentUserId)
+                            .filter(subscriber -> subscriber.mUserId != mCurrentUser)
                             .filter(subscriber -> subscriber.mUserId != UserHandle.USER_SYSTEM));
                 }
-                mCurrentUser = currentUserId;
+            }
+            bindToUserClients();
+        }
 
-                if (mUserManager.isUserUnlocked(mCurrentUser)) {
-                    bindToSystemClients();
+        @Override
+        public void onUserLockChanged(int userId, boolean unlocked) {
+            synchronized (mLock) {
+                if (mCurrentUser == userId && unlocked) {
                     bindToUserClients();
                 }
             }
@@ -137,33 +136,34 @@
      * Constructor for client manager.
      *
      * @param context           Context to use for registering receivers and binding services.
-     * @param brokerService     Service managing the VMS publisher/subscriber state.
+     * @param statsService      Service for logging client metrics.
      * @param userService       User service for registering system unlock listener.
-     * @param userManagerHelper User manager for querying current user state.
+     * @param brokerService     Service managing the VMS publisher/subscriber state.
      * @param halService        Service providing the HAL client interface
      */
-    public VmsClientManager(Context context, VmsBrokerService brokerService,
-            CarUserService userService, CarUserManagerHelper userManagerHelper,
+    public VmsClientManager(Context context, CarStatsService statsService,
+            CarUserService userService, VmsBrokerService brokerService,
             VmsHalService halService) {
-        this(context, brokerService, userService, userManagerHelper, halService,
-                Binder::getCallingUid);
+        this(context, statsService, userService, brokerService, halService,
+                new Handler(Looper.getMainLooper()), Binder::getCallingUid);
     }
 
     @VisibleForTesting
-    VmsClientManager(Context context, VmsBrokerService brokerService,
-            CarUserService userService, CarUserManagerHelper userManagerHelper,
-            VmsHalService halService, IntSupplier getCallingUid) {
+    VmsClientManager(Context context, CarStatsService statsService,
+            CarUserService userService, VmsBrokerService brokerService,
+            VmsHalService halService, Handler handler, IntSupplier getCallingUid) {
         mContext = context;
         mPackageManager = context.getPackageManager();
-        mHandler = new Handler(Looper.getMainLooper());
-        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mStatsService = statsService;
         mUserService = userService;
-        mUserManagerHelper = userManagerHelper;
-        mCurrentUser = mUserManagerHelper.getCurrentForegroundUserId();
+        mCurrentUser = UserHandle.USER_NULL;
         mBrokerService = brokerService;
-        mMillisBeforeRebind = mContext.getResources().getInteger(
-                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+        mHandler = handler;
         mGetCallingUid = getCallingUid;
+        mMillisBeforeRebind = context.getResources().getInteger(
+                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+
         halService.setClientManager(this);
     }
 
@@ -181,17 +181,12 @@
     @Override
     public void init() {
         mUserService.runOnUser0Unlock(mSystemUserUnlockedListener);
-
-        IntentFilter userSwitchFilter = new IntentFilter();
-        userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
-        userSwitchFilter.addAction(Intent.ACTION_USER_UNLOCKED);
-        mContext.registerReceiverAsUser(mUserSwitchReceiver, UserHandle.ALL, userSwitchFilter, null,
-                null);
+        mUserService.addUserCallback(mUserCallback);
     }
 
     @Override
     public void release() {
-        mContext.unregisterReceiver(mUserSwitchReceiver);
+        mUserService.removeUserCallback(mUserCallback);
         synchronized (mLock) {
             if (mHalClient != null) {
                 mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
@@ -204,11 +199,6 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        dumpMetrics(writer);
-    }
-
-    @Override
-    public void dumpMetrics(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
         synchronized (mLock) {
             writer.println("mCurrentUser:" + mCurrentUser);
@@ -224,12 +214,6 @@
                 writer.printf("\t%s\n", subscriber);
             }
         }
-        synchronized (mRebindCounts) {
-            writer.println("mRebindCounts:");
-            for (Map.Entry<String, AtomicLong> entry : mRebindCounts.entrySet()) {
-                writer.printf("\t%s: %s\n", entry.getKey(), entry.getValue());
-            }
-        }
     }
 
 
@@ -240,7 +224,8 @@
      */
     public void addSubscriber(IVmsSubscriberClient subscriberClient) {
         if (subscriberClient == null) {
-            Log.e(TAG, "Trying to add a null subscriber: " + getCallingPackage());
+            Log.e(TAG, "Trying to add a null subscriber: "
+                    + getCallingPackage(mGetCallingUid.getAsInt()));
             throw new IllegalArgumentException("subscriber cannot be null.");
         }
 
@@ -251,13 +236,14 @@
                 return;
             }
 
-            int subscriberUserId = UserHandle.getUserId(mGetCallingUid.getAsInt());
+            int callingUid = mGetCallingUid.getAsInt();
+            int subscriberUserId = UserHandle.getUserId(callingUid);
             if (subscriberUserId != mCurrentUser && subscriberUserId != UserHandle.USER_SYSTEM) {
                 throw new SecurityException("Caller must be foreground user or system");
             }
 
             SubscriberConnection subscriber = new SubscriberConnection(
-                    subscriberClient, getCallingPackage(), subscriberUserId);
+                    subscriberClient, callingUid, getCallingPackage(callingUid), subscriberUserId);
             if (DBG) Log.d(TAG, "Registering subscriber: " + subscriber);
             try {
                 subscriberBinder.linkToDeath(subscriber, 0);
@@ -286,9 +272,21 @@
      * Returns all active subscriber clients.
      */
     public Collection<IVmsSubscriberClient> getAllSubscribers() {
-        return mSubscribers.values().stream()
-                .map(subscriber -> subscriber.mClient)
-                .collect(Collectors.toList());
+        synchronized (mLock) {
+            return mSubscribers.values().stream()
+                    .map(subscriber -> subscriber.mClient)
+                    .collect(Collectors.toList());
+        }
+    }
+
+    /**
+     * Gets the application UID associated with a subscriber client.
+     */
+    public int getSubscriberUid(IVmsSubscriberClient subscriberClient) {
+        synchronized (mLock) {
+            SubscriberConnection subscriber = mSubscribers.get(subscriberClient.asBinder());
+            return subscriber != null ? subscriber.mUid : Process.INVALID_UID;
+        }
     }
 
     /**
@@ -303,9 +301,6 @@
 
     /**
      * Registers the HAL client connections.
-     *
-     * @param publisherClient
-     * @param subscriberClient
      */
     public void onHalConnected(IVmsPublisherClient publisherClient,
             IVmsSubscriberClient subscriberClient) {
@@ -313,9 +308,11 @@
             mHalClient = publisherClient;
             mPublisherService.onClientConnected(HAL_CLIENT_NAME, mHalClient);
             mSubscribers.put(subscriberClient.asBinder(),
-                    new SubscriberConnection(subscriberClient, HAL_CLIENT_NAME,
+                    new SubscriberConnection(subscriberClient, Process.myUid(), HAL_CLIENT_NAME,
                             UserHandle.USER_SYSTEM));
         }
+        mStatsService.getVmsClientLog(Process.myUid())
+                .logConnectionState(ConnectionState.CONNECTED);
     }
 
     /**
@@ -325,14 +322,13 @@
         synchronized (mLock) {
             if (mHalClient != null) {
                 mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
+                mStatsService.getVmsClientLog(Process.myUid())
+                        .logConnectionState(ConnectionState.DISCONNECTED);
             }
             mHalClient = null;
             terminate(mSubscribers.values().stream()
                     .filter(subscriber -> HAL_CLIENT_NAME.equals(subscriber.mPackageName)));
         }
-        synchronized (mRebindCounts) {
-            mRebindCounts.computeIfAbsent(HAL_CLIENT_NAME, k -> new AtomicLong()).incrementAndGet();
-        }
     }
 
     private void dumpConnections(PrintWriter writer,
@@ -359,15 +355,24 @@
     }
 
     private void bindToUserClients() {
+        bindToSystemClients(); // Bind system clients on user switch, if they are not already bound.
         synchronized (mLock) {
+            if (mCurrentUser == UserHandle.USER_NULL) {
+                Log.e(TAG, "Unknown user in foreground.");
+                return;
+            }
             // To avoid the risk of double-binding, clients running as the system user must only
             // ever be bound in bindToSystemClients().
-            // In a headless multi-user system, the system user will never be in the foreground.
             if (mCurrentUser == UserHandle.USER_SYSTEM) {
                 Log.e(TAG, "System user in foreground. Userspace clients will not be bound.");
                 return;
             }
 
+            if (!mUserManager.isUserUnlocked(mCurrentUser)) {
+                Log.i(TAG, "Waiting for foreground user to be unlocked.");
+                return;
+            }
+
             String[] clientNames = mContext.getResources().getStringArray(
                     R.array.vmsPublisherUserClients);
             Log.i(TAG, "Attempting to bind " + clientNames.length + " user client(s)");
@@ -400,13 +405,17 @@
             return;
         }
 
+        VmsClientLog statsLog = mStatsService.getVmsClientLog(
+                UserHandle.getUid(userHandle.getIdentifier(), serviceInfo.applicationInfo.uid));
+
         if (!Car.PERMISSION_BIND_VMS_CLIENT.equals(serviceInfo.permission)) {
             Log.e(TAG, "Client service: " + clientName
                     + " does not require " + Car.PERMISSION_BIND_VMS_CLIENT + " permission");
+            statsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
             return;
         }
 
-        PublisherConnection connection = new PublisherConnection(name, userHandle);
+        PublisherConnection connection = new PublisherConnection(name, userHandle, statsLog);
         if (connection.bind()) {
             Log.i(TAG, "Client bound: " + connection);
             connectionMap.put(clientName, connection);
@@ -424,15 +433,17 @@
         private final ComponentName mName;
         private final UserHandle mUser;
         private final String mFullName;
+        private final VmsClientLog mStatsLog;
         private boolean mIsBound = false;
         private boolean mIsTerminated = false;
         private boolean mRebindScheduled = false;
         private IVmsPublisherClient mClientService;
 
-        PublisherConnection(ComponentName name, UserHandle user) {
+        PublisherConnection(ComponentName name, UserHandle user, VmsClientLog statsLog) {
             mName = name;
             mUser = user;
             mFullName = mName.flattenToString() + " U=" + mUser.getIdentifier();
+            mStatsLog = statsLog;
         }
 
         synchronized boolean bind() {
@@ -442,6 +453,7 @@
             if (mIsTerminated) {
                 return false;
             }
+            mStatsLog.logConnectionState(ConnectionState.CONNECTING);
 
             if (DBG) Log.d(TAG, "binding: " + mFullName);
             Intent intent = new Intent();
@@ -453,6 +465,10 @@
                 Log.e(TAG, "While binding " + mFullName, e);
             }
 
+            if (!mIsBound) {
+                mStatsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
+            }
+
             return mIsBound;
         }
 
@@ -496,23 +512,20 @@
             // If the client is not currently bound, unbind() will have no effect.
             unbind();
             bind();
-            synchronized (mRebindCounts) {
-                mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
-                        .incrementAndGet();
-            }
         }
 
         synchronized void terminate() {
             if (DBG) Log.d(TAG, "terminating: " + mFullName);
             mIsTerminated = true;
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.TERMINATED);
             unbind();
         }
 
-        synchronized void notifyOnDisconnect() {
+        synchronized void notifyOnDisconnect(int connectionState) {
             if (mClientService != null) {
                 mPublisherService.onClientDisconnected(mFullName);
                 mClientService = null;
+                mStatsLog.logConnectionState(connectionState);
             }
         }
 
@@ -521,19 +534,20 @@
             if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
             mClientService = IVmsPublisherClient.Stub.asInterface(service);
             mPublisherService.onClientConnected(mFullName, mClientService);
+            mStatsLog.logConnectionState(ConnectionState.CONNECTED);
         }
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
             if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.DISCONNECTED);
             scheduleRebind();
         }
 
         @Override
         public void onBindingDied(ComponentName name) {
             if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.DISCONNECTED);
             scheduleRebind();
         }
 
@@ -549,8 +563,8 @@
     }
 
     // If we're in a binder call, returns back the package name of the caller of the binder call.
-    private String getCallingPackage() {
-        String packageName = mPackageManager.getNameForUid(mGetCallingUid.getAsInt());
+    private String getCallingPackage(int uid) {
+        String packageName = mPackageManager.getNameForUid(uid);
         if (packageName == null) {
             return UNKNOWN_PACKAGE;
         } else {
@@ -560,12 +574,14 @@
 
     private class SubscriberConnection implements IBinder.DeathRecipient {
         private final IVmsSubscriberClient mClient;
+        private final int mUid;
         private final String mPackageName;
         private final int mUserId;
 
-        SubscriberConnection(IVmsSubscriberClient subscriberClient, String packageName,
+        SubscriberConnection(IVmsSubscriberClient subscriberClient, int uid, String packageName,
                 int userId) {
             mClient = subscriberClient;
+            mUid = uid;
             mPackageName = packageName;
             mUserId = userId;
         }
diff --git a/tests/BugReportApp/AndroidManifest.xml b/tests/BugReportApp/AndroidManifest.xml
index 90958c7..d879396 100644
--- a/tests/BugReportApp/AndroidManifest.xml
+++ b/tests/BugReportApp/AndroidManifest.xml
@@ -16,8 +16,8 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.google.android.car.bugreport"
-          android:versionCode="8"
-          android:versionName="1.6.0">
+          android:versionCode="10"
+          android:versionName="1.6.2">
 
     <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
@@ -29,8 +29,11 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
     <uses-permission android:name="android.permission.DUMP"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
 
-    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+    <application android:label="@string/app_name"
+                 android:icon="@drawable/ic_launcher"
+                 android:requestLegacyExternalStorage="true">
         <activity android:name=".BugReportInfoActivity"
                   android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
                   android:exported="true"
@@ -39,6 +42,7 @@
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
                 <action android:name="android.intent.action.VIEW"/>
+                <action android:name="com.google.android.car.bugreport.action.START_SILENT"/>
             </intent-filter>
         </activity>
 
diff --git a/tests/BugReportApp/README.md b/tests/BugReportApp/README.md
index 2dac8e9..acd4347 100644
--- a/tests/BugReportApp/README.md
+++ b/tests/BugReportApp/README.md
@@ -48,6 +48,17 @@
 BugReport app uses `res/raw/gcs_credentials.json` for authentication and
 `res/values/configs.xml` for obtaining GCS bucket name.
 
+## Starting bugreporting
+
+The app supports following intents:
+
+1. `adb shell am start com.google.android.car.bugreport/com.google.android.car.bugreport.BugReportActivity`
+    - generates `MetaBugReport.Type.INTERACTIVE` bug report, with audio message, shows a
+    SUBMIT/CANCEL dialog.
+2. `adb shell am start -n com.google.android.car.bugreport/com.google.android.car.bugreport.BugReportActivity -a com.google.android.car.bugreport.action.START_SILENT`
+    - generates `MetaBugReport.Type.SILENT` bug report, without audio message. The app doesn't
+    auto-upload these bugreports.
+
 ## Testing
 
 ### Manually testing the app using the test script
diff --git a/tests/BugReportApp/res/layout/bug_info_view.xml b/tests/BugReportApp/res/layout/bug_info_view.xml
index 3736c00..089acb5 100644
--- a/tests/BugReportApp/res/layout/bug_info_view.xml
+++ b/tests/BugReportApp/res/layout/bug_info_view.xml
@@ -26,99 +26,59 @@
         android:orientation="vertical">
 
         <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="TITLE"
-            android:textColor="@android:color/holo_green_light"
-            android:textSize="28sp" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="USER"
-            android:textColor="@android:color/holo_red_dark"
-            android:textSize="28sp" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="BUG TIME"
-            android:textColor="@android:color/holo_red_light"
-            android:textSize="28sp" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="STATUS"
-            android:textColor="@android:color/holo_orange_light"
-            android:textSize="28sp" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="OTHER"
-            android:textColor="@android:color/holo_blue_dark"
-            android:textSize="28sp" />
-
-        <Button
-            android:id="@+id/bug_info_upload_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
-            android:textSize="@dimen/bug_report_button_text_size"
-            android:text="@string/bugreport_upload_button_text" />
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:orientation="vertical">
-
-        <TextView
             android:id="@+id/bug_info_row_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="left"
-            android:textSize="28sp" />
+            android:textColor="@*android:color/car_yellow_500"
+            android:textSize="@dimen/bug_report_default_text_size" />
 
-
-        <TextView
-            android:id="@+id/bug_info_row_user"
+        <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="28sp" />
-
-
-        <TextView
-            android:id="@+id/bug_info_row_timestamp"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="28sp" />
-
-
-        <TextView
-            android:id="@+id/bug_info_row_status"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="28sp" />
-
+            android:orientation="horizontal">
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="@dimen/bug_report_horizontal_layout_children_margin"
+                android:text="@string/bugreport_info_status"
+                android:textSize="@dimen/bug_report_default_text_size" />
+            <TextView
+                android:id="@+id/bug_info_row_status"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="@dimen/bug_report_default_text_size" />
+        </LinearLayout>
 
         <TextView
             android:id="@+id/bug_info_row_message"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="28sp" />
+            android:visibility="gone"
+            android:textSize="@dimen/bug_report_default_text_size" />
 
-        <Button
-            android:id="@+id/bug_info_move_button"
+        <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
-            android:textSize="@dimen/bug_report_button_text_size"
-            android:text="@string/bugreport_move_button_text" />
-
+            android:orientation="horizontal">
+            <Button
+                android:id="@+id/bug_info_upload_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+                android:layout_marginRight="@dimen/bug_report_horizontal_layout_children_margin"
+                android:visibility="gone"
+                android:textSize="@dimen/bug_report_button_text_size"
+                android:text="@string/bugreport_upload_button_text" />
+            <Button
+                android:id="@+id/bug_info_move_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+                android:visibility="gone"
+                android:textSize="@dimen/bug_report_button_text_size"
+                android:text="@string/bugreport_move_button_text" />
+        </LinearLayout>
     </LinearLayout>
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/tests/BugReportApp/res/values/dimens.xml b/tests/BugReportApp/res/values/dimens.xml
index 4a5f270..7f7967f 100644
--- a/tests/BugReportApp/res/values/dimens.xml
+++ b/tests/BugReportApp/res/values/dimens.xml
@@ -19,6 +19,8 @@
     <!-- Margin between edge of BugReportActivity dialog and content -->
     <dimen name="bug_report_padding">30dp</dimen>
 
+    <dimen name="bug_report_default_text_size">28dp</dimen>
+
     <!-- VoiceRecordingView dimensions -->
     <dimen name="bug_report_voice_recording_margin_top">20dp</dimen>
     <dimen name="bug_report_voice_recording_height">40dp</dimen>
@@ -34,4 +36,6 @@
     <!-- ProgressBar dimensions -->
     <dimen name="bug_report_progress_bar_margin_top">32dp</dimen>
 
+    <!-- Horizontal layout children margins. -->
+    <dimen name="bug_report_horizontal_layout_children_margin">12dp</dimen>
 </resources>
diff --git a/tests/BugReportApp/res/values/strings.xml b/tests/BugReportApp/res/values/strings.xml
index bca00fa..66680e2 100644
--- a/tests/BugReportApp/res/values/strings.xml
+++ b/tests/BugReportApp/res/values/strings.xml
@@ -15,35 +15,36 @@
      limitations under the License.
 -->
 <resources>
-    <string name="app_name" translatable="false">Bug Report</string>
+    <string name="app_name">Bug Report</string>
 
-    <string name="bugreport_info_quit" translatable="false">Close</string>
-    <string name="bugreport_info_start" translatable="false">Start Bug Report</string>
+    <string name="bugreport_info_quit">Close</string>
+    <string name="bugreport_info_start">Start Bug Report</string>
+    <string name="bugreport_info_status">Status:</string>
 
-    <string name="bugreport_dialog_submit" translatable="false">Submit</string>
-    <string name="bugreport_dialog_cancel" translatable="false">Cancel</string>
-    <string name="bugreport_dialog_show_bugreports" translatable="false">Show Bug Reports</string>
-    <string name="bugreport_dialog_close" translatable="false">Close</string>
-    <string name="bugreport_dialog_title" translatable="false">Speak &amp; Describe The Issue</string>
-    <string name="bugreport_dialog_recording_finished" translatable="false">Recording finished</string>
-    <string name="bugreport_dialog_in_progress_title" translatable="false">A bug report is already being collected</string>
-    <string name="bugreport_dialog_in_progress_title_finished" translatable="false">A bug report has been collected</string>
-    <string name="bugreport_move_button_text" translatable="false">Move</string>
-    <string name="bugreport_upload_button_text" translatable="false">Upload</string>
+    <string name="bugreport_dialog_submit">Submit</string>
+    <string name="bugreport_dialog_cancel">Cancel</string>
+    <string name="bugreport_dialog_show_bugreports">Show Bug Reports</string>
+    <string name="bugreport_dialog_close">Close</string>
+    <string name="bugreport_dialog_title">Speak &amp; Describe The Issue</string>
+    <string name="bugreport_dialog_recording_finished">Recording finished</string>
+    <string name="bugreport_dialog_in_progress_title">A bug report is already being collected</string>
+    <string name="bugreport_dialog_in_progress_title_finished">A bug report has been collected</string>
+    <string name="bugreport_move_button_text">Move to USB</string>
+    <string name="bugreport_upload_button_text">Upload to Google</string>
 
-    <string name="toast_permissions_denied" translatable="false">Please grant permissions</string>
-    <string name="toast_bug_report_in_progress" translatable="false">Bug report already being collected</string>
-    <string name="toast_timed_out" translatable="false">Timed out, cancelling</string>
-    <string name="toast_status_failed" translatable="false">Bug report failed</string>
-    <string name="toast_status_finished" translatable="false">Bug report finished</string>
-    <string name="toast_status_pending_upload" translatable="false">Bug report ready for upload</string>
-    <string name="toast_status_screencap_failed" translatable="false">Screen capture failed</string>
-    <string name="toast_status_dump_state_failed" translatable="false">Dump state failed</string>
+    <string name="toast_permissions_denied">Please grant permissions</string>
+    <string name="toast_bug_report_in_progress">Bug report already being collected</string>
+    <string name="toast_timed_out">Timed out, cancelling</string>
+    <string name="toast_status_failed">Bug report failed</string>
+    <string name="toast_status_finished">Bug report finished</string>
+    <string name="toast_status_pending_upload">Bug report ready for upload</string>
+    <string name="toast_status_screencap_failed">Screen capture failed</string>
+    <string name="toast_status_dump_state_failed">Dump state failed</string>
 
     <!-- Notification strings -->
-    <string name="notification_bugreport_in_progress" translatable="false">Bug report is in progress</string>
-    <string name="notification_bugreport_finished_title" translatable="false">Bug report is collected</string>
-    <string name="notification_bugreport_manual_upload_finished_text" translatable="false">Please upload it when the car is parked</string>
-    <string name="notification_bugreport_auto_upload_finished_text" translatable="false">The report will be upload automatically</string>
-    <string name="notification_bugreport_channel_name" translatable="false">Bug report status channel</string>
+    <string name="notification_bugreport_in_progress">Bug report is in progress</string>
+    <string name="notification_bugreport_finished_title">Bug report is collected</string>
+    <string name="notification_bugreport_manual_upload_finished_text">Please upload it when the car is parked</string>
+    <string name="notification_bugreport_auto_upload_finished_text">The report will be upload automatically</string>
+    <string name="notification_bugreport_channel_name">Bug report status channel</string>
 </resources>
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
index ad2e38f..07084fc 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
@@ -23,63 +23,69 @@
 
 import androidx.recyclerview.widget.RecyclerView;
 
+import java.util.ArrayList;
 import java.util.List;
 
+/**
+ * Shows bugreport title, status, status message and user action buttons. "Upload to Google" button
+ * is enabled when the status is {@link Status#STATUS_PENDING_USER_ACTION}, "Move to USB" button is
+ * enabled only when status is  {@link Status#STATUS_PENDING_USER_ACTION} and USB device is plugged
+ * in.
+ */
 public class BugInfoAdapter extends RecyclerView.Adapter<BugInfoAdapter.BugInfoViewHolder> {
-
     static final int BUTTON_TYPE_UPLOAD = 0;
     static final int BUTTON_TYPE_MOVE = 1;
 
     /** Provides a handler for click events*/
     interface ItemClickedListener {
-        /** onItemClicked handles click events differently depending on provided buttonType and
-         * uses additional information provided in metaBugReport. */
-        void onItemClicked(int buttonType, MetaBugReport metaBugReport);
+        /**
+         * Handles click events differently depending on provided buttonType and
+         * uses additional information provided in metaBugReport.
+         *
+         * @param buttonType One of {@link #BUTTON_TYPE_UPLOAD} or {@link #BUTTON_TYPE_MOVE}.
+         * @param metaBugReport Selected bugreport.
+         * @param holder ViewHolder of the clicked item.
+         */
+        void onItemClicked(int buttonType, MetaBugReport metaBugReport, BugInfoViewHolder holder);
     }
 
     /**
      * Reference to each bug report info views.
      */
-    public static class BugInfoViewHolder extends RecyclerView.ViewHolder {
+    static class BugInfoViewHolder extends RecyclerView.ViewHolder {
         /** Title view */
-        public TextView titleView;
-
-        /** User view */
-        public TextView userView;
-
-        /** TimeStamp View */
-        public TextView timestampView;
+        TextView mTitleView;
 
         /** Status View */
-        public TextView statusView;
+        TextView mStatusView;
 
         /** Message View */
-        public TextView messageView;
+        TextView mMessageView;
 
         /** Move Button */
-        public Button moveButton;
+        Button mMoveButton;
 
         /** Upload Button */
-        public Button uploadButton;
+        Button mUploadButton;
 
         BugInfoViewHolder(View v) {
             super(v);
-            titleView = itemView.findViewById(R.id.bug_info_row_title);
-            userView = itemView.findViewById(R.id.bug_info_row_user);
-            timestampView = itemView.findViewById(R.id.bug_info_row_timestamp);
-            statusView = itemView.findViewById(R.id.bug_info_row_status);
-            messageView = itemView.findViewById(R.id.bug_info_row_message);
-            moveButton = itemView.findViewById(R.id.bug_info_move_button);
-            uploadButton = itemView.findViewById(R.id.bug_info_upload_button);
+            mTitleView = itemView.findViewById(R.id.bug_info_row_title);
+            mStatusView = itemView.findViewById(R.id.bug_info_row_status);
+            mMessageView = itemView.findViewById(R.id.bug_info_row_message);
+            mMoveButton = itemView.findViewById(R.id.bug_info_move_button);
+            mUploadButton = itemView.findViewById(R.id.bug_info_upload_button);
         }
     }
 
-    private final List<MetaBugReport> mDataset;
+    private List<MetaBugReport> mDataset;
     private final ItemClickedListener mItemClickedListener;
 
-    BugInfoAdapter(List<MetaBugReport> dataSet, ItemClickedListener itemClickedListener) {
-        mDataset = dataSet;
+    BugInfoAdapter(ItemClickedListener itemClickedListener) {
         mItemClickedListener = itemClickedListener;
+        mDataset = new ArrayList<>();
+        // Allow RecyclerView to efficiently update UI; getItemId() is implemented below.
+        setHasStableIds(true);
     }
 
     @Override
@@ -93,22 +99,54 @@
     @Override
     public void onBindViewHolder(BugInfoViewHolder holder, int position) {
         MetaBugReport bugreport = mDataset.get(position);
-        holder.titleView.setText(mDataset.get(position).getTitle());
-        holder.userView.setText(mDataset.get(position).getUsername());
-        holder.timestampView.setText(mDataset.get(position).getTimestamp());
-        holder.statusView.setText(Status.toString(mDataset.get(position).getStatus()));
-        holder.messageView.setText(mDataset.get(position).getStatusMessage());
-        if (bugreport.getStatus() == Status.STATUS_PENDING_USER_ACTION.getValue()
-                || bugreport.getStatus() == Status.STATUS_MOVE_FAILED.getValue()
-                || bugreport.getStatus() == Status.STATUS_UPLOAD_FAILED.getValue()) {
-            holder.moveButton.setOnClickListener(
-                    view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_MOVE, bugreport));
-            holder.uploadButton.setOnClickListener(
-                    view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport));
+        holder.mTitleView.setText(bugreport.getTitle());
+        holder.mStatusView.setText(Status.toString(bugreport.getStatus()));
+        holder.mMessageView.setText(bugreport.getStatusMessage());
+        if (bugreport.getStatusMessage().isEmpty()) {
+            holder.mMessageView.setVisibility(View.GONE);
         } else {
-            holder.moveButton.setEnabled(false);
-            holder.uploadButton.setEnabled(false);
+            holder.mMessageView.setVisibility(View.VISIBLE);
         }
+        boolean enableUserActionButtons =
+                bugreport.getStatus() == Status.STATUS_PENDING_USER_ACTION.getValue()
+                        || bugreport.getStatus() == Status.STATUS_MOVE_FAILED.getValue()
+                        || bugreport.getStatus() == Status.STATUS_UPLOAD_FAILED.getValue();
+        if (enableUserActionButtons) {
+            holder.mMoveButton.setEnabled(true);
+            holder.mMoveButton.setVisibility(View.VISIBLE);
+            holder.mMoveButton.setOnClickListener(
+                    view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_MOVE, bugreport,
+                            holder));
+            holder.mUploadButton.setEnabled(true);
+            holder.mUploadButton.setVisibility(View.VISIBLE);
+            holder.mUploadButton.setOnClickListener(
+                    view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport,
+                            holder));
+        } else {
+            holder.mMoveButton.setVisibility(View.GONE);
+            holder.mMoveButton.setEnabled(false);
+            holder.mUploadButton.setVisibility(View.GONE);
+            holder.mUploadButton.setEnabled(false);
+        }
+    }
+
+    /** Sets dataSet; it copies the list, because it modifies it in this adapter. */
+    void setDataset(List<MetaBugReport> bugReports) {
+        mDataset = new ArrayList<>(bugReports);
+        notifyDataSetChanged();
+    }
+
+    /** Update a bug report in the data set. */
+    void updateBugReportInDataSet(MetaBugReport bugReport, int position) {
+        if (position != RecyclerView.NO_POSITION) {
+            mDataset.set(position, bugReport);
+            notifyItemChanged(position);
+        }
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return mDataset.get(position).getId();
     }
 
     @Override
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
index 456192e..d8597d8 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
@@ -25,6 +25,7 @@
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateManager;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
@@ -61,10 +62,18 @@
  * submit button it initiates {@link BugReportService}.
  *
  * <p>If bug report is in-progress, it shows a progress bar.
+ *
+ * <p>If the activity is started with action {@link #ACTION_START_SILENT}, it will start
+ * bugreporting without showing dialog and recording audio message, see
+ * {@link MetaBugReport#TYPE_SILENT}.
  */
 public class BugReportActivity extends Activity {
     private static final String TAG = BugReportActivity.class.getSimpleName();
 
+    /** Starts headless (no audio message recording) bugreporting. */
+    private static final String ACTION_START_SILENT =
+            "com.google.android.car.bugreport.action.START_SILENT";
+
     private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000;
     private static final int AUDIO_PERMISSIONS_REQUEST_ID = 1;
 
@@ -132,6 +141,14 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        if (ACTION_START_SILENT.equals(getIntent().getAction())) {
+            Log.i(TAG, "Starting headless bugreport.");
+            MetaBugReport bugReport = createBugReport(this, MetaBugReport.TYPE_SILENT);
+            startBugReportingInService(this, bugReport);
+            finish();
+            return;
+        }
+
         requestWindowFeature(Window.FEATURE_NO_TITLE);
         setContentView(R.layout.bug_report_activity);
 
@@ -261,11 +278,7 @@
         mAudioRecordingStarted = true;
         showSubmitBugReportUi(/* isRecording= */ true);
 
-        Date initiatedAt = new Date();
-        String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
-        String username = getCurrentUserName();
-        String title = BugReportTitleGenerator.generateBugReportTitle(initiatedAt, username);
-        mMetaBugReport = BugStorageUtils.createBugReport(this, title, timestamp, username);
+        mMetaBugReport = createBugReport(this, MetaBugReport.TYPE_INTERACTIVE);
 
         if (!hasRecordPermissions()) {
             requestRecordPermissions();
@@ -294,7 +307,9 @@
     }
 
     private void buttonSubmitClick(View view) {
-        startBugReportingInService();
+        stopAudioRecording();
+        startBugReportingInService(this, mMetaBugReport);
+        mBugReportServiceStarted = true;
         finish();
     }
 
@@ -314,16 +329,6 @@
         finish();
     }
 
-    private void startBugReportingInService() {
-        stopAudioRecording();
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(EXTRA_META_BUG_REPORT, mMetaBugReport);
-        Intent intent = new Intent(this, BugReportService.class);
-        intent.putExtras(bundle);
-        startService(intent);
-        mBugReportServiceStarted = true;
-    }
-
     private void requestRecordPermissions() {
         requestPermissions(
                 new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_PERMISSIONS_REQUEST_ID);
@@ -430,11 +435,33 @@
         mVoiceRecordingView.setRecorder(null);
     }
 
-    private String getCurrentUserName() {
-        UserManager um = UserManager.get(this);
+    private static void startBugReportingInService(Context context, MetaBugReport bugReport) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(EXTRA_META_BUG_REPORT, bugReport);
+        Intent intent = new Intent(context, BugReportService.class);
+        intent.putExtras(bundle);
+        context.startForegroundService(intent);
+    }
+
+    private static String getCurrentUserName(Context context) {
+        UserManager um = UserManager.get(context);
         return um.getUserName();
     }
 
+    /**
+     * Creates a {@link MetaBugReport} and saves it in a local sqlite database.
+     *
+     * @param context an Android context.
+     * @param type bug report type, {@link MetaBugReport.BugReportType}.
+     */
+    private static MetaBugReport createBugReport(Context context, int type) {
+        Date initiatedAt = new Date();
+        String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
+        String username = getCurrentUserName(context);
+        String title = BugReportTitleGenerator.generateBugReportTitle(initiatedAt, username);
+        return BugStorageUtils.createBugReport(context, title, timestamp, username, type);
+    }
+
     /** A helper class to generate bugreport title. */
     private static final class BugReportTitleGenerator {
         /** Contains easily readable characters. */
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
index 0469bb1..46b5d45 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
@@ -21,9 +21,13 @@
 import android.app.NotificationManager;
 import android.content.ContentResolver;
 import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
 import android.provider.DocumentsContract;
 import android.util.Log;
 import android.view.View;
@@ -47,102 +51,16 @@
 public class BugReportInfoActivity extends Activity {
     public static final String TAG = BugReportInfoActivity.class.getSimpleName();
 
+    /** Used for moving bug reports to a new location (e.g. USB drive). */
     private static final int SELECT_DIRECTORY_REQUEST_CODE = 1;
 
     private RecyclerView mRecyclerView;
-    private RecyclerView.Adapter mAdapter;
+    private BugInfoAdapter mBugInfoAdapter;
     private RecyclerView.LayoutManager mLayoutManager;
     private NotificationManager mNotificationManager;
     private MetaBugReport mLastSelectedBugReport;
-
-    private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, Boolean> {
-        private final BugReportInfoActivity mActivity;
-        private final MetaBugReport mBugReport;
-        private final Uri mDestinationDirUri;
-
-        AsyncMoveFilesTask(BugReportInfoActivity activity, MetaBugReport bugReport,
-                Uri destinationDir) {
-            mActivity = activity;
-            mBugReport = bugReport;
-            mDestinationDirUri = destinationDir;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... params) {
-            Uri sourceUri = BugStorageProvider.buildUriWithBugId(mBugReport.getId());
-            ContentResolver resolver = mActivity.getContentResolver();
-            String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
-            Uri parentDocumentUri =
-                    DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
-            String mimeType = resolver.getType(sourceUri);
-            try {
-                Uri newFileUri = DocumentsContract.createDocument(resolver, parentDocumentUri,
-                        mimeType,
-                        new File(mBugReport.getFilePath()).toPath().getFileName().toString());
-                if (newFileUri == null) {
-                    Log.e(TAG, "Unable to create a new file.");
-                    return false;
-                }
-                try (InputStream input = resolver.openInputStream(sourceUri);
-                     OutputStream output = resolver.openOutputStream(newFileUri)) {
-                    byte[] buffer = new byte[4096];
-                    int len;
-                    while ((len = input.read(buffer)) > 0) {
-                        output.write(buffer, 0, len);
-                    }
-                }
-                BugStorageUtils.setBugReportStatus(
-                        mActivity, mBugReport,
-                        com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL, "");
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to create the bug report in the location.", e);
-                return false;
-            }
-            return true;
-        }
-
-        @Override
-        protected void onPostExecute(Boolean moveSuccessful) {
-            if (!moveSuccessful) {
-                BugStorageUtils.setBugReportStatus(
-                        mActivity, mBugReport,
-                        com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, "");
-            }
-            // Refresh the UI to reflect the new status.
-            new BugReportInfoTask(mActivity).execute();
-        }
-    }
-
-    private static final class BugReportInfoTask extends
-            AsyncTask<Void, Void, List<MetaBugReport>> {
-        private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
-
-        BugReportInfoTask(BugReportInfoActivity activity) {
-            mBugReportInfoActivityWeakReference = new WeakReference<>(activity);
-        }
-
-        @Override
-        protected List<MetaBugReport> doInBackground(Void... voids) {
-            BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
-            if (activity == null) {
-                Log.w(TAG, "Activity is gone, cancelling BugReportInfoTask.");
-                return new ArrayList<>();
-            }
-            return BugStorageUtils.getAllBugReportsDescending(activity);
-        }
-
-        @Override
-        protected void onPostExecute(List<MetaBugReport> result) {
-            BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
-            if (activity == null) {
-                Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
-                return;
-            }
-            activity.mAdapter = new BugInfoAdapter(result, activity::onBugReportItemClicked);
-            activity.mRecyclerView.setAdapter(activity.mAdapter);
-            activity.mRecyclerView.getAdapter().notifyDataSetChanged();
-        }
-    }
+    private BugInfoAdapter.BugInfoViewHolder mLastSelectedBugInfoViewHolder;
+    private BugStorageObserver mBugStorageObserver;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -159,9 +77,10 @@
         mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
                 DividerItemDecoration.VERTICAL));
 
-        // specify an adapter (see also next example)
-        mAdapter = new BugInfoAdapter(new ArrayList<>(), this::onBugReportItemClicked);
-        mRecyclerView.setAdapter(mAdapter);
+        mBugInfoAdapter = new BugInfoAdapter(this::onBugReportItemClicked);
+        mRecyclerView.setAdapter(mBugInfoAdapter);
+
+        mBugStorageObserver = new BugStorageObserver(this, new Handler());
 
         findViewById(R.id.quit_button).setOnClickListener(this::onQuitButtonClick);
         findViewById(R.id.start_bug_report_button).setOnClickListener(
@@ -175,7 +94,16 @@
     @Override
     protected void onStart() {
         super.onStart();
-        new BugReportInfoTask(this).execute();
+        new BugReportsLoaderAsyncTask(this).execute();
+        // As BugStorageProvider is running under user0, we register using USER_ALL.
+        getContentResolver().registerContentObserver(BugStorageProvider.BUGREPORT_CONTENT_URI, true,
+                mBugStorageObserver, UserHandle.USER_ALL);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        getContentResolver().unregisterContentObserver(mBugStorageObserver);
     }
 
     /**
@@ -186,15 +114,17 @@
         mNotificationManager.cancel(BugReportService.BUGREPORT_FINISHED_NOTIF_ID);
     }
 
-    private void onBugReportItemClicked(int buttonType, MetaBugReport bugReport) {
+    private void onBugReportItemClicked(
+            int buttonType, MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder) {
         if (buttonType == BugInfoAdapter.BUTTON_TYPE_UPLOAD) {
             Log.i(TAG, "Uploading " + bugReport.getFilePath());
             BugStorageUtils.setBugReportStatus(this, bugReport, Status.STATUS_UPLOAD_PENDING, "");
             // Refresh the UI to reflect the new status.
-            new BugReportInfoTask(this).execute();
+            new BugReportsLoaderAsyncTask(this).execute();
         } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_MOVE) {
             Log.i(TAG, "Moving " + bugReport.getFilePath());
             mLastSelectedBugReport = bugReport;
+            mLastSelectedBugInfoViewHolder = holder;
             startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),
                     SELECT_DIRECTORY_REQUEST_CODE);
         } else {
@@ -211,11 +141,20 @@
                             | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
             Uri destDirUri = data.getData();
             getContentResolver().takePersistableUriPermission(destDirUri, takeFlags);
-            if (mLastSelectedBugReport == null) {
+            if (mLastSelectedBugReport == null || mLastSelectedBugInfoViewHolder == null) {
                 Log.w(TAG, "No bug report is selected.");
                 return;
             }
-            new AsyncMoveFilesTask(this, mLastSelectedBugReport, destDirUri).execute();
+            MetaBugReport updatedBugReport = BugStorageUtils.setBugReportStatus(this,
+                    mLastSelectedBugReport, Status.STATUS_MOVE_IN_PROGRESS, "");
+            mBugInfoAdapter.updateBugReportInDataSet(
+                    updatedBugReport, mLastSelectedBugInfoViewHolder.getAdapterPosition());
+            new AsyncMoveFilesTask(
+                this,
+                    mBugInfoAdapter,
+                    updatedBugReport,
+                    mLastSelectedBugInfoViewHolder,
+                    destDirUri).execute();
         }
     }
 
@@ -230,4 +169,137 @@
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         startActivity(intent);
     }
+
+    /** Observer for {@link BugStorageProvider}. */
+    private static class BugStorageObserver extends ContentObserver {
+        private final BugReportInfoActivity mInfoActivity;
+
+        /**
+         * Creates a content observer.
+         *
+         * @param activity A {@link BugReportInfoActivity} instance.
+         * @param handler The handler to run {@link #onChange} on, or null if none.
+         */
+        BugStorageObserver(BugReportInfoActivity activity, Handler handler) {
+            super(handler);
+            mInfoActivity = activity;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            new BugReportsLoaderAsyncTask(mInfoActivity).execute();
+        }
+    }
+
+    /** Moves bugreport zip to a new location and updates RecyclerView. */
+    private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, MetaBugReport> {
+        private static final int COPY_BUFFER_SIZE = 4096; // bytes
+
+        private final BugReportInfoActivity mActivity;
+        private final MetaBugReport mBugReport;
+        private final Uri mDestinationDirUri;
+        /** RecyclerView.Adapter that contains all the bug reports. */
+        private final BugInfoAdapter mBugInfoAdapter;
+        /** ViewHolder for {@link #mBugReport}. */
+        private final BugInfoAdapter.BugInfoViewHolder mBugViewHolder;
+
+        AsyncMoveFilesTask(BugReportInfoActivity activity, BugInfoAdapter bugInfoAdapter,
+                MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder,
+                Uri destinationDir) {
+            mActivity = activity;
+            mBugInfoAdapter = bugInfoAdapter;
+            mBugReport = bugReport;
+            mBugViewHolder = holder;
+            mDestinationDirUri = destinationDir;
+        }
+
+        /** Moves the bugreport to the USB drive and returns the updated {@link MetaBugReport}. */
+        @Override
+        protected MetaBugReport doInBackground(Void... params) {
+            Uri sourceUri = BugStorageProvider.buildUriWithBugId(mBugReport.getId());
+            ContentResolver resolver = mActivity.getContentResolver();
+            String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
+            Uri parentDocumentUri =
+                    DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
+            String mimeType = resolver.getType(sourceUri);
+            try {
+                Uri newFileUri = DocumentsContract.createDocument(resolver, parentDocumentUri,
+                        mimeType,
+                        new File(mBugReport.getFilePath()).toPath().getFileName().toString());
+                if (newFileUri == null) {
+                    Log.e(TAG, "Unable to create a new file.");
+                    return BugStorageUtils.setBugReportStatus(
+                            mActivity,
+                            mBugReport,
+                            com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED,
+                            "Unable to create a new file.");
+                }
+                try (InputStream input = resolver.openInputStream(sourceUri);
+                     AssetFileDescriptor fd = resolver.openAssetFileDescriptor(newFileUri, "w")) {
+                    OutputStream output = fd.createOutputStream();
+                    byte[] buffer = new byte[COPY_BUFFER_SIZE];
+                    int len;
+                    while ((len = input.read(buffer)) > 0) {
+                        output.write(buffer, 0, len);
+                    }
+                    // Force sync the written data from memory to the disk.
+                    fd.getFileDescriptor().sync();
+                }
+                Log.d(TAG, "Finished writing to " + newFileUri.getPath());
+                if (BugStorageUtils.deleteBugReportZipfile(mActivity, mBugReport.getId())) {
+                    Log.i(TAG, "Local bugreport zip is deleted.");
+                } else {
+                    Log.w(TAG, "Failed to delete the local bugreport " + mBugReport.getFilePath());
+                }
+                BugStorageUtils.setBugReportStatus(
+                        mActivity, mBugReport,
+                        com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL, "");
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to create the bug report in the location.", e);
+                return BugStorageUtils.setBugReportStatus(
+                        mActivity, mBugReport,
+                        com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, e);
+            }
+            return BugStorageUtils.setBugReportStatus(mActivity, mBugReport,
+                        com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL,
+                        "Moved to: " + mDestinationDirUri.getPath());
+        }
+
+        @Override
+        protected void onPostExecute(MetaBugReport updatedBugReport) {
+            // Refresh the UI to reflect the new status.
+            mBugInfoAdapter.updateBugReportInDataSet(
+                    updatedBugReport, mBugViewHolder.getAdapterPosition());
+        }
+    }
+
+    /** Asynchronously loads bugreports from {@link BugStorageProvider}. */
+    private static final class BugReportsLoaderAsyncTask extends
+            AsyncTask<Void, Void, List<MetaBugReport>> {
+        private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
+
+        BugReportsLoaderAsyncTask(BugReportInfoActivity activity) {
+            mBugReportInfoActivityWeakReference = new WeakReference<>(activity);
+        }
+
+        @Override
+        protected List<MetaBugReport> doInBackground(Void... voids) {
+            BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
+            if (activity == null) {
+                Log.w(TAG, "Activity is gone, cancelling BugReportsLoaderAsyncTask.");
+                return new ArrayList<>();
+            }
+            return BugStorageUtils.getAllBugReportsDescending(activity);
+        }
+
+        @Override
+        protected void onPostExecute(List<MetaBugReport> result) {
+            BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
+            if (activity == null) {
+                Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
+                return;
+            }
+            activity.mBugInfoAdapter.setDataset(result);
+        }
+    }
 }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
index f6fc651..cab945b 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -213,6 +213,9 @@
     }
 
     private Notification buildProgressNotification() {
+        Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
+        PendingIntent startBugReportInfoActivity =
+                PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
         return new Notification.Builder(this, PROGRESS_CHANNEL_ID)
                 .setContentTitle(getText(R.string.notification_bugreport_in_progress))
                 .setSubText(String.format("%.1f%%", mBugReportProgress.get()))
@@ -220,6 +223,7 @@
                 .setCategory(Notification.CATEGORY_STATUS)
                 .setOngoing(true)
                 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false)
+                .setContentIntent(startBugReportInfoActivity)
                 .build();
     }
 
@@ -346,12 +350,16 @@
         Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
         PendingIntent startBugReportInfoActivity =
                 PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
+        CharSequence contentText;
+        if (JobSchedulingUtils.autoUploadBugReport(mMetaBugReport)) {
+            contentText = getText(R.string.notification_bugreport_auto_upload_finished_text);
+        } else {
+            contentText = getText(R.string.notification_bugreport_manual_upload_finished_text);
+        }
         Notification notification = new Notification
                 .Builder(getApplicationContext(), STATUS_CHANNEL_ID)
                 .setContentTitle(getText(R.string.notification_bugreport_finished_title))
-                .setContentText(getText(JobSchedulingUtils.uploadByDefault()
-                        ? R.string.notification_bugreport_auto_upload_finished_text
-                        : R.string.notification_bugreport_manual_upload_finished_text))
+                .setContentText(contentText)
                 .setCategory(Notification.CATEGORY_STATUS)
                 .setSmallIcon(R.drawable.ic_upload)
                 .setContentIntent(startBugReportInfoActivity)
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
index 7a4b3de..92c1697 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
@@ -33,6 +33,9 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
 
 
 /**
@@ -46,9 +49,12 @@
 
     private static final String AUTHORITY = "com.google.android.car.bugreport";
     private static final String BUG_REPORTS_TABLE = "bugreports";
+    private static final String URL_SEGMENT_DELETE_ZIP_FILE = "deleteZipFile";
+
     static final Uri BUGREPORT_CONTENT_URI =
             Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE);
 
+    /** See {@link MetaBugReport} for column descriptions. */
     static final String COLUMN_ID = "_ID";
     static final String COLUMN_USERNAME = "username";
     static final String COLUMN_TITLE = "title";
@@ -57,10 +63,12 @@
     static final String COLUMN_FILEPATH = "filepath";
     static final String COLUMN_STATUS = "status";
     static final String COLUMN_STATUS_MESSAGE = "message";
+    static final String COLUMN_TYPE = "type";
 
     // URL Matcher IDs.
     private static final int URL_MATCHED_BUG_REPORTS_URI = 1;
     private static final int URL_MATCHED_BUG_REPORT_ID_URI = 2;
+    private static final int URL_MATCHED_DELETE_ZIP_FILE = 3;
 
     private Handler mHandler;
 
@@ -78,9 +86,11 @@
         /**
          * All changes in database versions should be recorded here.
          * 1: Initial version.
+         * 2: Add integer column details_needed.
          */
         private static final int INITIAL_VERSION = 1;
-        private static final int DATABASE_VERSION = INITIAL_VERSION;
+        private static final int TYPE_VERSION = 2;
+        private static final int DATABASE_VERSION = TYPE_VERSION;
 
         private static final String CREATE_TABLE = "CREATE TABLE " + BUG_REPORTS_TABLE + " ("
                 + COLUMN_ID + " INTEGER PRIMARY KEY,"
@@ -90,7 +100,8 @@
                 + COLUMN_DESCRIPTION + " TEXT NULL,"
                 + COLUMN_FILEPATH + " TEXT DEFAULT NULL,"
                 + COLUMN_STATUS + " INTEGER DEFAULT " + Status.STATUS_WRITE_PENDING.getValue() + ","
-                + COLUMN_STATUS_MESSAGE + " TEXT NULL"
+                + COLUMN_STATUS_MESSAGE + " TEXT NULL,"
+                + COLUMN_TYPE + " INTEGER DEFAULT " + MetaBugReport.TYPE_INTERACTIVE
                 + ");";
 
         DatabaseHelper(Context context) {
@@ -105,6 +116,10 @@
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             Log.w(TAG, "Upgrading from " + oldVersion + " to " + newVersion);
+            if (oldVersion == INITIAL_VERSION && newVersion == TYPE_VERSION) {
+                db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+                        + COLUMN_TYPE + " INTEGER DEFAULT " + MetaBugReport.TYPE_INTERACTIVE);
+            }
         }
     }
 
@@ -113,10 +128,19 @@
         return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/" + bugReportId);
     }
 
+    /** Builds {@link Uri} that deletes a zip file for given bugreport id. */
+    static Uri buildUriDeleteZipFile(int bugReportId) {
+        return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/"
+                + URL_SEGMENT_DELETE_ZIP_FILE + "/" + bugReportId);
+    }
+
     public BugStorageProvider() {
         mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
         mUriMatcher.addURI(AUTHORITY, BUG_REPORTS_TABLE, URL_MATCHED_BUG_REPORTS_URI);
         mUriMatcher.addURI(AUTHORITY, BUG_REPORTS_TABLE + "/#", URL_MATCHED_BUG_REPORT_ID_URI);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_DELETE_ZIP_FILE + "/#",
+                URL_MATCHED_DELETE_ZIP_FILE);
     }
 
     @Override
@@ -213,6 +237,7 @@
     @Override
     public int delete(
             @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+        SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
         switch (mUriMatcher.match(uri)) {
             //  returns the bugreport that match the id.
             case URL_MATCHED_BUG_REPORT_ID_URI:
@@ -222,12 +247,23 @@
                 }
                 selection = COLUMN_ID + " = ?";
                 selectionArgs = new String[]{uri.getLastPathSegment()};
-                break;
+                // Ignore the results of zip file deletion, likelihood of failure is too small.
+                deleteZipFile(Integer.parseInt(uri.getLastPathSegment()));
+                getContext().getContentResolver().notifyChange(uri, null);
+                return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
+            case URL_MATCHED_DELETE_ZIP_FILE:
+                if (selection != null || selectionArgs != null) {
+                    throw new IllegalArgumentException("selection is not allowed for "
+                            + URL_MATCHED_DELETE_ZIP_FILE);
+                }
+                if (deleteZipFile(Integer.parseInt(uri.getLastPathSegment()))) {
+                    getContext().getContentResolver().notifyChange(uri, null);
+                    return 1;
+                }
+                return 0;
             default:
                 throw new IllegalArgumentException("Unknown URL " + uri);
         }
-        SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
-        return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
     }
 
     @Override
@@ -276,27 +312,12 @@
             throw new IllegalArgumentException("unknown uri:" + uri);
         }
 
-        Cursor c = query(uri, new String[]{COLUMN_FILEPATH}, null, null, null);
-        int count = (c != null) ? c.getCount() : 0;
-        if (count != 1) {
-            // If there is not exactly one result, throw an appropriate
-            // exception.
-            if (c != null) {
-                c.close();
-            }
-            if (count == 0) {
-                throw new FileNotFoundException("No entry for " + uri);
-            }
-            throw new FileNotFoundException("Multiple items at " + uri);
-        }
-
-        c.moveToFirst();
-        int i = c.getColumnIndex(COLUMN_FILEPATH);
-        String path = (i >= 0 ? c.getString(i) : null);
-        c.close();
-        if (path == null) {
-            throw new FileNotFoundException("Column for path not found.");
-        }
+        // Because we expect URI to be URL_MATCHED_BUG_REPORT_ID_URI.
+        int bugreportId = Integer.parseInt(uri.getLastPathSegment());
+        MetaBugReport bugReport =
+                BugStorageUtils.findBugReport(getContext(), bugreportId)
+                        .orElseThrow(() -> new FileNotFoundException("No record found for " + uri));
+        String path = bugReport.getFilePath();
 
         int modeBits = ParcelFileDescriptor.parseMode(mode);
         try {
@@ -312,8 +333,9 @@
                 Status status;
                 if (e == null) {
                     // success writing the file. Update the field to indicate bugreport
-                    // is ready for upload
-                    status = JobSchedulingUtils.uploadByDefault() ? Status.STATUS_UPLOAD_PENDING
+                    // is ready for upload.
+                    status = JobSchedulingUtils.autoUploadBugReport(bugReport)
+                            ? Status.STATUS_UPLOAD_PENDING
                             : Status.STATUS_PENDING_USER_ACTION;
                 } else {
                     // We log it and ignore it
@@ -329,6 +351,7 @@
                     JobSchedulingUtils.scheduleUploadJob(BugStorageProvider.this.getContext());
                 }
                 Log.i(TAG, "Finished adding bugreport " + path + " " + uri);
+                getContext().getContentResolver().notifyChange(uri, null);
             });
         } catch (IOException e) {
             // An IOException (for example not being able to open the file, will crash us.
@@ -336,4 +359,29 @@
             throw new RuntimeException(e);
         }
     }
+
+    private boolean deleteZipFile(int bugreportId) {
+        Optional<MetaBugReport> bugReport =
+                BugStorageUtils.findBugReport(getContext(), bugreportId);
+        if (!bugReport.isPresent()) {
+            Log.i(TAG,
+                    "Failed to delete zip file for bug " + bugreportId + ": bugreport not found.");
+            return false;
+        }
+        Path zipPath = new File(bugReport.get().getFilePath()).toPath();
+        // This statement is to prevent the app to print confusing full FileNotFound error stack
+        // when the user is navigating from BugReportActivity (audio message recording dialog) to
+        // BugReportInfoActivity by clicking "Show Bug Reports" button.
+        if (!Files.exists(zipPath)) {
+            Log.w(TAG, "Failed to delete " + zipPath + ". File not found.");
+            return false;
+        }
+        try {
+            Files.delete(zipPath);
+            return true;
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to delete zip file " + zipPath, e);
+            return false;
+        }
+    }
 }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
index cbb1a3c..4cdc7cd 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
@@ -21,6 +21,7 @@
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_STATUS_MESSAGE;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TIMESTAMP;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TITLE;
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TYPE;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_USERNAME;
 
 import android.annotation.NonNull;
@@ -30,7 +31,6 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.text.TextUtils;
 import android.util.Log;
 
 import com.google.api.client.auth.oauth2.TokenResponseException;
@@ -42,6 +42,7 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * A class that hides details when communicating with the bug storage provider.
@@ -68,6 +69,7 @@
      * @param title     - title of the bug report.
      * @param timestamp - timestamp when the bug report was initiated.
      * @param username  - current user name. Note, it's a user name, not an account name.
+     * @param type      - bug report type, {@link MetaBugReport.BugReportType}.
      * @return an instance of {@link MetaBugReport} that was created in a database.
      */
     @NonNull
@@ -75,12 +77,14 @@
             @NonNull Context context,
             @NonNull String title,
             @NonNull String timestamp,
-            @NonNull String username) {
+            @NonNull String username,
+            @MetaBugReport.BugReportType int type) {
         // insert bug report username and title
         ContentValues values = new ContentValues();
         values.put(COLUMN_TITLE, title);
         values.put(COLUMN_TIMESTAMP, timestamp);
         values.put(COLUMN_USERNAME, username);
+        values.put(COLUMN_TYPE, type);
 
         ContentResolver r = context.getContentResolver();
         Uri uri = r.insert(BugStorageProvider.BUGREPORT_CONTENT_URI, values);
@@ -96,6 +100,7 @@
         return new MetaBugReport.Builder(id, timestamp)
                 .setTitle(title)
                 .setUserName(username)
+                .setType(type)
                 .build();
     }
 
@@ -132,6 +137,12 @@
         return r.delete(BugStorageProvider.buildUriWithBugId(bugReportId), null, null) == 1;
     }
 
+    /** Deletes zip file for given bugreport id; doesn't delete sqlite3 record. */
+    static boolean deleteBugReportZipfile(@NonNull Context context, int bugReportId) {
+        ContentResolver r = context.getContentResolver();
+        return r.delete(BugStorageProvider.buildUriDeleteZipFile(bugReportId), null, null) == 1;
+    }
+
     /**
      * Returns bugreports that are waiting to be uploaded.
      */
@@ -152,8 +163,20 @@
         return getBugreports(context, null, null, COLUMN_ID + " DESC");
     }
 
-    private static List<MetaBugReport> getBugreports(Context context, String selection,
-            String[] selectionArgs, String order) {
+    /** Returns {@link MetaBugReport} for given bugreport id. */
+    static Optional<MetaBugReport> findBugReport(Context context, int bugreportId) {
+        String selection = COLUMN_ID + " = ?";
+        String[] selectionArgs = new String[]{Integer.toString(bugreportId)};
+        List<MetaBugReport> bugs = BugStorageUtils.getBugreports(
+                context, selection, selectionArgs, null);
+        if (bugs.isEmpty()) {
+            return Optional.empty();
+        }
+        return Optional.of(bugs.get(0));
+    }
+
+    private static List<MetaBugReport> getBugreports(
+            Context context, String selection, String[] selectionArgs, String order) {
         ArrayList<MetaBugReport> bugReports = new ArrayList<>();
         String[] projection = {
                 COLUMN_ID,
@@ -162,7 +185,8 @@
                 COLUMN_TIMESTAMP,
                 COLUMN_FILEPATH,
                 COLUMN_STATUS,
-                COLUMN_STATUS_MESSAGE};
+                COLUMN_STATUS_MESSAGE,
+                COLUMN_TYPE};
         ContentResolver r = context.getContentResolver();
         Cursor c = r.query(BugStorageProvider.BUGREPORT_CONTENT_URI, projection,
                 selection, selectionArgs, order);
@@ -178,6 +202,7 @@
                     .setFilepath(getString(c, COLUMN_FILEPATH))
                     .setStatus(getInt(c, COLUMN_STATUS))
                     .setStatusMessage(getString(c, COLUMN_STATUS_MESSAGE))
+                    .setType(getInt(c, COLUMN_TYPE))
                     .build();
             bugReports.add(meta);
             c.moveToNext();
@@ -219,13 +244,6 @@
     }
 
     /**
-     * Sets bugreport status to upload failed.
-     */
-    public static void setUploadFailed(Context context, MetaBugReport bugReport, Exception e) {
-        setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_FAILED, getRootCauseMessage(e));
-    }
-
-    /**
      * Sets bugreport status pending, and update the message to last exception message.
      *
      * <p>Used when a transient error has occurred.
@@ -260,18 +278,37 @@
         return t.getMessage();
     }
 
-    /** Updates bug report record status. */
-    static void setBugReportStatus(
+    /**
+     * Updates bug report record status.
+     *
+     * @return Updated {@link MetaBugReport}.
+     */
+    static MetaBugReport setBugReportStatus(
             Context context, MetaBugReport bugReport, Status status, String message) {
         // update status
         ContentValues values = new ContentValues();
         values.put(COLUMN_STATUS, status.getValue());
-        if (!TextUtils.isEmpty(message)) {
-            values.put(COLUMN_STATUS_MESSAGE, message);
-        }
+        values.put(COLUMN_STATUS_MESSAGE, message);
         String where = COLUMN_ID + "=" + bugReport.getId();
-        context.getContentResolver().update(BugStorageProvider.BUGREPORT_CONTENT_URI, values,
-                where, null);
+        int updatedRows = context.getContentResolver().update(
+                BugStorageProvider.BUGREPORT_CONTENT_URI, values, where, null);
+        if (updatedRows == 1) {
+            return bugReport.toBuilder()
+                    .setStatus(status.getValue())
+                    .setStatusMessage(message)
+                    .build();
+        }
+        return bugReport;
+    }
+
+    /**
+     * Updates bug report record status.
+     *
+     * @return Updated {@link MetaBugReport}.
+     */
+    static MetaBugReport setBugReportStatus(
+            Context context, MetaBugReport bugReport, Status status, Exception e) {
+        return setBugReportStatus(context, bugReport, status, getRootCauseMessage(e));
     }
 
     private static String currentTimestamp() {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
index 7a6de23..95c4685 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
@@ -80,7 +80,12 @@
      * after bugreport is successfully written. A user then has an option to choose whether to
      * upload the bugreport or copy it to an external drive.
      */
-    static boolean uploadByDefault() {
+    private static boolean uploadByDefault() {
         return !SystemProperties.getBoolean(PROP_DISABLE_AUTO_UPLOAD, false);
     }
+
+    /** Returns {@code true} if bugreports has to be auto-uploaded. */
+    static boolean autoUploadBugReport(MetaBugReport bugReport) {
+        return uploadByDefault() && bugReport.getType() == MetaBugReport.TYPE_INTERACTIVE;
+    }
 }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
index d309303..6d5e89b 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
@@ -15,11 +15,35 @@
  */
 package com.google.android.car.bugreport;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.google.common.base.Strings;
+
+import java.lang.annotation.Retention;
+
 /** Represents the information that a bugreport can contain. */
 public final class MetaBugReport implements Parcelable {
+
+    /** Contains {@link #TYPE_SILENT} and audio message. */
+    static final int TYPE_INTERACTIVE = 0;
+
+    /**
+     * Contains dumpstate and screenshots.
+     *
+     * <p>Silent bugreports are not uploaded automatically. The app asks user to add audio
+     * message either through notification or {@link BugReportInfoActivity}.
+     */
+    static final int TYPE_SILENT = 1;
+
+    /** Annotation for bug report types. */
+    @Retention(SOURCE)
+    @IntDef({TYPE_INTERACTIVE, TYPE_SILENT})
+    @interface BugReportType {};
+
     private final int mId;
     private final String mTimestamp;
     private final String mTitle;
@@ -27,6 +51,8 @@
     private final String mFilePath;
     private final int mStatus;
     private final String mStatusMessage;
+    /** One of {@link BugReportType}. */
+    private final int mType;
 
     private MetaBugReport(Builder builder) {
         mId = builder.mId;
@@ -36,6 +62,7 @@
         mFilePath = builder.mFilePath;
         mStatus = builder.mStatus;
         mStatusMessage = builder.mStatusMessage;
+        mType = builder.mType;
     }
 
     /**
@@ -49,32 +76,32 @@
      * @return Username (LDAP) that created this bugreport
      */
     public String getUsername() {
-        return mUsername == null ? "" : mUsername;
+        return Strings.nullToEmpty(mUsername);
     }
 
     /**
      * @return Title of the bug.
      */
     public String getTitle() {
-        return mTitle == null ? "" : mTitle;
+        return Strings.nullToEmpty(mTitle);
     }
 
     /**
      * @return Timestamp when the bug report is initialized.
      */
     public String getTimestamp() {
-        return mTimestamp == null ? "" : mTimestamp;
+        return Strings.nullToEmpty(mTimestamp);
     }
 
     /**
      * @return path to the zip file
      */
     public String getFilePath() {
-        return mFilePath == null ? "" : mFilePath;
+        return Strings.nullToEmpty(mFilePath);
     }
 
     /**
-     * @return Status of the bug upload.
+     * @return {@link Status} of the bug upload.
      */
     public int getStatus() {
         return mStatus;
@@ -84,7 +111,14 @@
      * @return StatusMessage of the bug upload.
      */
     public String getStatusMessage() {
-        return mStatusMessage == null ? "" : mStatusMessage;
+        return Strings.nullToEmpty(mStatusMessage);
+    }
+
+    /**
+     * @return {@link BugReportType}.
+     */
+    public int getType() {
+        return mType;
     }
 
     @Override
@@ -92,6 +126,17 @@
         return 0;
     }
 
+    /** Returns {@link Builder} from the meta bug report. */
+    public Builder toBuilder() {
+        return new Builder(mId, mTimestamp)
+                .setFilepath(mFilePath)
+                .setStatus(mStatus)
+                .setStatusMessage(mStatusMessage)
+                .setTitle(mTitle)
+                .setUserName(mUsername)
+                .setType(mType);
+    }
+
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mId);
@@ -101,6 +146,7 @@
         dest.writeString(mFilePath);
         dest.writeInt(mStatus);
         dest.writeString(mStatusMessage);
+        dest.writeInt(mType);
     }
 
     /** A creator that's used by Parcelable. */
@@ -114,12 +160,14 @@
                     String filePath = in.readString();
                     int status = in.readInt();
                     String statusMessage = in.readString();
+                    int type = in.readInt();
                     return new Builder(id, timestamp)
                             .setTitle(title)
                             .setUserName(username)
                             .setFilepath(filePath)
                             .setStatus(status)
                             .setStatusMessage(statusMessage)
+                            .setType(type)
                             .build();
                 }
 
@@ -137,6 +185,7 @@
         private String mFilePath;
         private int mStatus;
         private String mStatusMessage;
+        private int mType;
 
         /**
          * Initializes MetaBugReport.Builder.
@@ -167,7 +216,7 @@
             return this;
         }
 
-        /** Sets status. */
+        /** Sets {@link Status}. */
         public Builder setStatus(int status) {
             mStatus = status;
             return this;
@@ -179,6 +228,12 @@
             return this;
         }
 
+        /** Sets the {@link BugReportType}. */
+        public Builder setType(@BugReportType int type) {
+            mType = type;
+            return this;
+        }
+
         /** Returns a {@link MetaBugReport}. */
         public MetaBugReport build() {
             return new MetaBugReport(this);
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
index 9142b91..67aa0ca 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
@@ -42,7 +42,10 @@
     STATUS_MOVE_SUCCESSFUL(7),
 
     // Bugreport move has failed.
-    STATUS_MOVE_FAILED(8);
+    STATUS_MOVE_FAILED(8),
+
+    // Bugreport is moving to USB drive.
+    STATUS_MOVE_IN_PROGRESS(9);
 
     private final int mValue;
 
@@ -76,6 +79,8 @@
                 return "Move successful";
             case 8:
                 return "Move failed";
+            case 9:
+                return "Move in progress";
         }
         return "unknown";
     }
diff --git a/tests/BugReportApp/utils/bugreport_app_tester.py b/tests/BugReportApp/utils/bugreport_app_tester.py
index 656f2d7..baf8ada 100755
--- a/tests/BugReportApp/utils/bugreport_app_tester.py
+++ b/tests/BugReportApp/utils/bugreport_app_tester.py
@@ -63,6 +63,10 @@
 STATUS_UPLOAD_SUCCESS = 3
 STATUS_UPLOAD_FAILED = 4
 STATUS_USER_CANCELLED = 5
+STATUS_PENDING_USER_ACTION = 6
+STATUS_MOVE_SUCCESSFUL = 7
+STATUS_MOVE_FAILED = 8
+STATUS_MOVE_IN_PROGRESS = 9
 
 DUMPSTATE_DEADLINE_SEC = 300  # 10 minutes.
 UPLOAD_DEADLINE_SEC = 180  # 3 minutes.
@@ -120,6 +124,14 @@
     return 'UPLOAD_FAILED'
   elif status == STATUS_USER_CANCELLED:
     return 'USER_CANCELLED'
+  elif status == STATUS_PENDING_USER_ACTION:
+    return 'PENDING_USER_ACTION'
+  elif status == STATUS_MOVE_SUCCESSFUL:
+    return 'MOVE_SUCCESSFUL'
+  elif status == STATUS_MOVE_FAILED:
+    return 'MOVE_FAILED'
+  elif status == STATUS_MOVE_IN_PROGRESS:
+    return 'MOVE_IN_PROGRESS'
   return 'UNKNOWN_STATUS'
 
 
@@ -337,7 +349,7 @@
             _bugreport_status_to_str(meta_bugreport.status))
 
   def _wait_for_bugreport_to_complete(self, bugreport_id):
-    """Waits until status changes to UPLOAD_PENDING.
+    """Waits until status changes to WRITE_PENDING.
 
     It means dumpstate (bugreport) is completed (or failed).
 
@@ -356,13 +368,17 @@
     print('\nDumpstate (bugreport) completed (or failed).')
 
   def _wait_for_bugreport_to_upload(self, bugreport_id):
-    """Waits bugreport to be uploaded and returns None if succeeds."""
+    """Waits bugreport to be uploaded and returns None if succeeds.
+
+    NOTE: If "android.car.bugreport.disableautoupload" system property is set,
+    the App will not upload.
+    """
     print('\nWaiting for the bug report to be uploaded.')
     err_msg = self._wait_for_bugreport_status_to_change_to(
         STATUS_UPLOAD_SUCCESS,
         UPLOAD_DEADLINE_SEC,
         bugreport_id,
-        allowed_statuses=[STATUS_UPLOAD_PENDING])
+        allowed_statuses=[STATUS_UPLOAD_PENDING, STATUS_PENDING_USER_ACTION])
     if err_msg:
       print('Failed to upload: %s' % err_msg)
       return err_msg
diff --git a/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java b/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
index 20fcfc0..71b3c2b 100644
--- a/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
+++ b/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
@@ -33,6 +33,7 @@
 
         View view = getLayoutInflater().inflate(R.layout.launcher_activity, null);
         setContentView(view);
+        reportFullyDrawn();
     }
 }
 
diff --git a/tests/CarDeveloperOptions/AndroidManifest.xml b/tests/CarDeveloperOptions/AndroidManifest.xml
index 5975572..046b386 100644
--- a/tests/CarDeveloperOptions/AndroidManifest.xml
+++ b/tests/CarDeveloperOptions/AndroidManifest.xml
@@ -21,7 +21,6 @@
     <original-package android:name="com.android.car.developeroptions" />
 
     <uses-permission android:name="android.permission.REQUEST_NETWORK_SCORES" />
-    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
diff --git a/tests/CarDeveloperOptions/res/xml/development_settings.xml b/tests/CarDeveloperOptions/res/xml/development_settings.xml
index 9ed1285..a946526 100644
--- a/tests/CarDeveloperOptions/res/xml/development_settings.xml
+++ b/tests/CarDeveloperOptions/res/xml/development_settings.xml
@@ -30,11 +30,6 @@
             android:summary="@string/summary_placeholder"
             android:fragment="com.android.car.developeroptions.applications.ProcessStatsSummary" />
 
-        <com.android.car.developeroptions.BugreportPreference
-            android:key="bugreport"
-            android:title="@*android:string/bugreport_title"
-            android:dialogTitle="@*android:string/bugreport_title" />
-
         <Preference
             android:key="system_server_heap_dump"
             android:title="@string/capture_system_heap_dump_title" />
@@ -151,11 +146,6 @@
             android:summary="@string/enable_terminal_summary" />
 
         <SwitchPreference
-            android:key="bugreport_in_power"
-            android:title="@string/bugreport_in_power"
-            android:summary="@string/bugreport_in_power_summary" />
-
-        <SwitchPreference
             android:key="automatic_system_server_heap_dumps"
             android:title="@string/automatic_system_heap_dump_title"
             android:summary="@string/automatic_system_heap_dump_summary" />
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java
deleted file mode 100644
index 6acef70..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.developeroptions;
-
-import android.app.ActivityManager;
-import android.app.settings.SettingsEnums;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.RemoteException;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.widget.CheckedTextView;
-import android.widget.TextView;
-
-import androidx.appcompat.app.AlertDialog.Builder;
-
-import com.android.car.developeroptions.overlay.FeatureFactory;
-import com.android.settingslib.CustomDialogPreferenceCompat;
-
-public class BugreportPreference extends CustomDialogPreferenceCompat {
-
-    private static final String TAG = "BugreportPreference";
-
-    private CheckedTextView mInteractiveTitle;
-    private TextView mInteractiveSummary;
-    private CheckedTextView mFullTitle;
-    private TextView mFullSummary;
-
-    public BugreportPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) {
-        super.onPrepareDialogBuilder(builder, listener);
-
-        final View dialogView = View.inflate(getContext(), R.layout.bugreport_options_dialog, null);
-        mInteractiveTitle = (CheckedTextView) dialogView.findViewById(R.id.bugreport_option_interactive_title);
-        mInteractiveSummary = (TextView) dialogView.findViewById(R.id.bugreport_option_interactive_summary);
-        mFullTitle = (CheckedTextView) dialogView.findViewById(R.id.bugreport_option_full_title);
-        mFullSummary = (TextView) dialogView.findViewById(R.id.bugreport_option_full_summary);
-        final View.OnClickListener l = new View.OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                if (v == mFullTitle || v == mFullSummary) {
-                    mInteractiveTitle.setChecked(false);
-                    mFullTitle.setChecked(true);
-                }
-                if (v == mInteractiveTitle || v == mInteractiveSummary) {
-                    mInteractiveTitle.setChecked(true);
-                    mFullTitle.setChecked(false);
-                }
-            }
-        };
-        mInteractiveTitle.setOnClickListener(l);
-        mFullTitle.setOnClickListener(l);
-        mInteractiveSummary.setOnClickListener(l);
-        mFullSummary.setOnClickListener(l);
-
-        builder.setPositiveButton(com.android.internal.R.string.report, listener);
-        builder.setView(dialogView);
-    }
-
-    @Override
-    protected void onClick(DialogInterface dialog, int which) {
-        if (which == DialogInterface.BUTTON_POSITIVE) {
-
-            final Context context = getContext();
-            if (mFullTitle.isChecked()) {
-                Log.v(TAG, "Taking full bugreport right away");
-                FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
-                        SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_FULL);
-                takeBugreport(ActivityManager.BUGREPORT_OPTION_FULL);
-            } else {
-                Log.v(TAG, "Taking interactive bugreport right away");
-                FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
-                        SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_INTERACTIVE);
-                takeBugreport(ActivityManager.BUGREPORT_OPTION_INTERACTIVE);
-            }
-        }
-    }
-
-    private void takeBugreport(int bugreportType) {
-        try {
-            ActivityManager.getService().requestBugReport(bugreportType);
-        } catch (RemoteException e) {
-            Log.e(TAG, "error taking bugreport (bugreportType=" + bugreportType + ")", e);
-        }
-    }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.java
deleted file mode 100644
index 1f22eb2..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.developeroptions.development;
-
-import android.content.Context;
-import android.os.UserManager;
-import android.provider.Settings;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.SwitchPreference;
-
-import com.android.car.developeroptions.core.PreferenceControllerMixin;
-import com.android.settingslib.development.DeveloperOptionsPreferenceController;
-
-public class BugReportInPowerPreferenceController extends
-        DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener,
-        PreferenceControllerMixin {
-
-    private static final String KEY_BUGREPORT_IN_POWER = "bugreport_in_power";
-
-    @VisibleForTesting
-    static int SETTING_VALUE_ON = 1;
-    @VisibleForTesting
-    static int SETTING_VALUE_OFF = 0;
-
-    private final UserManager mUserManager;
-
-    public BugReportInPowerPreferenceController(Context context) {
-        super(context);
-        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES);
-    }
-
-    @Override
-    public String getPreferenceKey() {
-        return KEY_BUGREPORT_IN_POWER;
-    }
-
-    @Override
-    public boolean onPreferenceChange(Preference preference, Object newValue) {
-        final boolean isEnabled = (Boolean) newValue;
-        Settings.Secure.putInt(mContext.getContentResolver(),
-                Settings.Global.BUGREPORT_IN_POWER_MENU,
-                isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
-        return true;
-    }
-
-    @Override
-    public void updateState(Preference preference) {
-        final int mode = Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Global.BUGREPORT_IN_POWER_MENU, SETTING_VALUE_OFF);
-        ((SwitchPreference) mPreference).setChecked(mode != SETTING_VALUE_OFF);
-    }
-
-    @Override
-    protected void onDeveloperOptionsSwitchDisabled() {
-        super.onDeveloperOptionsSwitchDisabled();
-        Settings.Secure.putInt(mContext.getContentResolver(),
-                Settings.Global.BUGREPORT_IN_POWER_MENU, SETTING_VALUE_OFF);
-        ((SwitchPreference) mPreference).setChecked(false);
-    }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java
deleted file mode 100644
index 28fb9b5..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.developeroptions.development;
-
-import android.content.Context;
-import android.os.UserManager;
-
-import com.android.car.developeroptions.core.PreferenceControllerMixin;
-import com.android.settingslib.development.DeveloperOptionsPreferenceController;
-
-public class BugReportPreferenceController extends DeveloperOptionsPreferenceController implements
-        PreferenceControllerMixin {
-
-    private static final String KEY_BUGREPORT = "bugreport";
-
-    private final UserManager mUserManager;
-
-    public BugReportPreferenceController(Context context) {
-        super(context);
-
-        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES);
-    }
-
-    @Override
-    public String getPreferenceKey() {
-        return KEY_BUGREPORT;
-    }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
index 95c8b6a..3253d21 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
@@ -403,7 +403,6 @@
             BluetoothA2dpConfigStore bluetoothA2dpConfigStore) {
         final List<AbstractPreferenceController> controllers = new ArrayList<>();
         controllers.add(new MemoryUsagePreferenceController(context));
-        controllers.add(new BugReportPreferenceController(context));
         controllers.add(new SystemServerHeapDumpPreferenceController(context));
         controllers.add(new LocalBackupPasswordPreferenceController(context));
         controllers.add(new StayAwakePreferenceController(context, lifecycle));
@@ -418,7 +417,6 @@
         controllers.add(new AdbPreferenceController(context, fragment));
         controllers.add(new ClearAdbKeysPreferenceController(context, fragment));
         controllers.add(new LocalTerminalPreferenceController(context));
-        controllers.add(new BugReportInPowerPreferenceController(context));
         controllers.add(new AutomaticSystemServerHeapDumpPreferenceController(context));
         controllers.add(new MockLocationAppPreferenceController(context, fragment));
         controllers.add(new DebugViewAttributesPreferenceController(context));
diff --git a/tests/CarTrustAgentClientApp/Android.mk b/tests/CarTrustAgentClientApp/Android.mk
deleted file mode 100644
index 3504ff7..0000000
--- a/tests/CarTrustAgentClientApp/Android.mk
+++ /dev/null
@@ -1,25 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := CarTrustAgentClient
-
-LOCAL_USE_AAPT2 := true
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    androidx.appcompat_appcompat \
-    androidx-constraintlayout_constraintlayout \
-    androidx.legacy_legacy-support-v4
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := optional
-LOCAL_MIN_SDK_VERSION := 23
-LOCAL_SDK_VERSION := current
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_PACKAGE)
diff --git a/tests/CarTrustAgentClientApp/AndroidManifest.xml b/tests/CarTrustAgentClientApp/AndroidManifest.xml
deleted file mode 100644
index e76485f..0000000
--- a/tests/CarTrustAgentClientApp/AndroidManifest.xml
+++ /dev/null
@@ -1,53 +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.
-  -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.car.trust.client">
-
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
-
-    <!-- Need Bluetooth LE -->
-    <uses-feature android:name="android.hardware.bluetooth_le"  android:required="true" />
-
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-
-    <!-- Needed to unlock user -->
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
-    <uses-permission android:name="android.permission.MANAGE_USERS" />
-    <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
-    <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
-    <application
-        android:label="@string/app_name"
-        android:theme="@style/Theme.AppCompat">
-
-        <activity
-                android:name=".PhoneEnrolmentActivity"
-                android:label="@string/app_name"
-                android:exported="true"
-                android:screenOrientation="portrait"
-                android:launchMode="singleInstance">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/tests/CarTrustAgentClientApp/README.txt b/tests/CarTrustAgentClientApp/README.txt
deleted file mode 100644
index bf6c444..0000000
--- a/tests/CarTrustAgentClientApp/README.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-IMPORTANT NOTE: This is a reference app to smart unlock paired HU during development.
-Consider moving the functionality to a more proper place.
diff --git a/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml b/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml
deleted file mode 100644
index 7237dfa..0000000
--- a/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml
+++ /dev/null
@@ -1,70 +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:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:weightSum="1">
-    <ScrollView
-        android:id="@+id/scroll"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:scrollbars="vertical"
-        android:layout_weight="0.80">
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:id="@+id/output"/>
-    </ScrollView>
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="0.10"
-        android:orientation="horizontal">
-        <Button
-            android:id="@+id/enroll_scan"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="2"
-            android:text="@string/enroll_scan"/>
-        <Button
-            android:id="@+id/enroll_button"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="3"
-            android:text="@string/enroll_button"/>
-    </LinearLayout>
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="0.10"
-        android:orientation="horizontal">
-        <Button
-            android:id="@+id/unlock_scan"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="2"
-            android:text="@string/unlock_scan"/>
-        <Button
-            android:id="@+id/unlock_button"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="3"
-            android:text="@string/unlock_button"/>
-    </LinearLayout>
-</LinearLayout>
diff --git a/tests/CarTrustAgentClientApp/res/values/strings.xml b/tests/CarTrustAgentClientApp/res/values/strings.xml
deleted file mode 100644
index 6e33a81..0000000
--- a/tests/CarTrustAgentClientApp/res/values/strings.xml
+++ /dev/null
@@ -1,37 +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.
-  -->
-<resources>
-    <string name="app_name" translatable="false">CarTrustAgentClient</string>
-
-    <!-- service/characteristics uuid for unlocking a device -->
-    <string name="unlock_service_uuid" translatable="false">5e2a68a1-27be-43f9-8d1e-4546976fabd7</string>
-    <string name="unlock_escrow_token_uiid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
-    <string name="unlock_handle_uiid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
-
-    <!-- service/characteristics uuid for adding new escrow token -->
-    <string name="enrollment_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
-    <string name="enrollment_handle_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
-    <string name="enrollment_token_uuid" translatable="false">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string>
-
-    <string name="pref_key_token_handle" translatable="false">token-handle-key</string>
-    <string name="pref_key_escrow_token" translatable="false">escrow-token-key</string>
-
-    <string name="enroll_button" translatable="false">Enroll new token</string>
-    <string name="enroll_scan" translatable="false">Scan to enroll</string>
-    <string name="unlock_button" translatable="false">Unlock</string>
-    <string name="unlock_scan" translatable="false">Scan to unlock</string>
-</resources>
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java
deleted file mode 100644
index 77ed7bb..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java
+++ /dev/null
@@ -1,51 +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.android.car.trust.client;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import java.util.UUID;
-
-/**
- * A utility class holding methods related to Bluetooth.
- */
-public class BluetoothUtils {
-    private BluetoothUtils() {}
-
-    /**
-     * Returns a characteristic off the given {@link BluetoothGattService} mapped to the jUUID
-     * specified. If the given service has multiple characteristics of the same UUID, then the
-     * first instance is returned.
-     *
-     * @param  uuidRes The unique identifier for the characteristic.
-     * @param  service The {@link BluetoothGattService} that contains the characteristic.
-     * @param  context The current {@link Context}.
-     * @return A {@link BluetoothGattCharacteristic} with a UUID matching {@code uuidRes} or
-     * {@code null} if none exists.
-     *
-     * @see BluetoothGattService#getCharacteristic(UUID)
-     */
-    @Nullable
-    public static BluetoothGattCharacteristic getCharacteristic(@StringRes int uuidRes,
-            BluetoothGattService service, Context context) {
-        return service.getCharacteristic(UUID.fromString(context.getString(uuidRes)));
-    }
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java
deleted file mode 100644
index fd29624..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java
+++ /dev/null
@@ -1,62 +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.android.car.trust.client;
-
-import android.Manifest;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-
-/**
- * Activity to allow the user to add an escrow token to a remote device. <p/>
- *
- * For this to work properly, the correct permissions must be set in the system config.  In AOSP,
- * this config is in frameworks/base/core/res/res/values/config.xml <p/>
- *
- * The config must set config_allowEscrowTokenForTrustAgent to true.  For the desired car
- * experience, the config should also set config_strongAuthRequiredOnBoot to false.
- */
-public class PhoneEnrolmentActivity extends FragmentActivity {
-
-    private static final int FINE_LOCATION_REQUEST_CODE = 42;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.phone_enrolment_activity);
-
-        PhoneEnrolmentController enrolmentController = new PhoneEnrolmentController(this);
-        enrolmentController.bind(findViewById(R.id.output), findViewById(R.id.enroll_scan),
-                findViewById(R.id.enroll_button));
-
-        PhoneUnlockController unlockController = new PhoneUnlockController(this);
-        unlockController.bind(findViewById(R.id.output), findViewById(R.id.unlock_scan),
-                findViewById(R.id.unlock_button));
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
-                != PackageManager.PERMISSION_GRANTED) {
-            requestPermissions(
-                    new String[] { android.Manifest.permission.ACCESS_FINE_LOCATION },
-                    FINE_LOCATION_REQUEST_CODE);
-        }
-    }
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java
deleted file mode 100644
index 1d3f672..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java
+++ /dev/null
@@ -1,197 +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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.preference.PreferenceManager;
-import android.util.Base64;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-
-import java.nio.ByteBuffer;
-import java.util.Random;
-import java.util.UUID;
-
-/**
- * A controller that sets up a {@link SimpleBleClient} to connect to the BLE enrollment service.
- * It also binds the UI components to control the enrollment process.
- */
-public class PhoneEnrolmentController {
-    private static final String TAG = "PhoneEnrollmentCltr";
-
-    private final String mTokenHandleKey;
-    private final String mEscrowTokenKey;
-
-    private final ParcelUuid mEnrolmentServiceUuid;
-
-    private final SimpleBleClient mClient;
-    private final Context mContext;
-    private final Handler mHandler;
-
-    // BLE characteristics associated with the enrollment/add escrow token service.
-    private BluetoothGattCharacteristic mEnrolmentTokenHandle;
-    private BluetoothGattCharacteristic mEnrolmentEscrowToken;
-
-    private TextView mTextView;
-    private Button mEnrolButton;
-
-    public PhoneEnrolmentController(Context context) {
-        mContext = context;
-
-        mTokenHandleKey = context.getString(R.string.pref_key_token_handle);
-        mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token);
-
-        mClient = new SimpleBleClient(context);
-        mEnrolmentServiceUuid = new ParcelUuid(
-                UUID.fromString(mContext.getString(R.string.enrollment_service_uuid)));
-        mClient.addCallback(mCallback /* callback */);
-
-        mHandler = new Handler(mContext.getMainLooper());
-    }
-
-    /**
-     * Binds the views to the actions that can be performed by this controller.
-     *
-     * @param textView    A text view used to display results from various BLE actions
-     * @param scanButton  Button used to start scanning for available BLE devices.
-     * @param enrolButton Button used to send new escrow token to remote device.
-     */
-    public void bind(TextView textView, Button scanButton, Button enrolButton) {
-        mTextView = textView;
-        mEnrolButton = enrolButton;
-
-        scanButton.setOnClickListener(v -> mClient.start(mEnrolmentServiceUuid));
-
-        mEnrolButton.setEnabled(false);
-        mEnrolButton.setAlpha(0.3f);
-        mEnrolButton.setOnClickListener(v -> {
-            appendOutputText("Sending new escrow token to remote device");
-
-            byte[] token = generateEscrowToken();
-            sendEnrolmentRequest(token);
-
-            // WARNING: Store the token so it can be used later for unlocking. This token
-            // should NEVER be stored on the device that is being unlocked. It should
-            // always be securely stored on a remote device that will trigger the unlock.
-            storeToken(token);
-        });
-    }
-
-    /**
-     * @return A random byte array that is used as the escrow token for remote device unlock.
-     */
-    private byte[] generateEscrowToken() {
-        Random random = new Random();
-        ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
-        buffer.putLong(0, random.nextLong());
-        return buffer.array();
-    }
-
-    private void sendEnrolmentRequest(byte[] token) {
-        mEnrolmentEscrowToken.setValue(token);
-        mClient.writeCharacteristic(mEnrolmentEscrowToken);
-        storeToken(token);
-    }
-
-    private void storeHandle(long handle) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
-        prefs.edit().putLong(mTokenHandleKey, handle).apply();
-    }
-
-    private void storeToken(byte[] token) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
-        String byteArray = Base64.encodeToString(token, Base64.DEFAULT);
-        prefs.edit().putString(mEscrowTokenKey, byteArray).apply();
-    }
-
-    private void appendOutputText(final String text) {
-        mHandler.post(() -> mTextView.append("\n" + text));
-    }
-
-    private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() {
-        @Override
-        public void onDeviceConnected(BluetoothDevice device) {
-            appendOutputText("Device connected: " + device.getName()
-                    + " addr: " + device.getAddress());
-        }
-
-        @Override
-        public void onDeviceDisconnected() {
-            appendOutputText("Device disconnected");
-        }
-
-        @Override
-        public void onCharacteristicChanged(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onCharacteristicChanged: "
-                        + convertToLong(characteristic.getValue()));
-            }
-
-            if (characteristic.getUuid().equals(mEnrolmentTokenHandle.getUuid())) {
-                // Store the new token handle that the BLE server is sending us. This required
-                // to unlock the device.
-                long handle = convertToLong(characteristic.getValue());
-                storeHandle(handle);
-                appendOutputText("Token handle received: " + handle);
-            }
-        }
-
-        @Override
-        public void onServiceDiscovered(BluetoothGattService service) {
-            if (!service.getUuid().equals(mEnrolmentServiceUuid.getUuid())) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Service UUID: " + service.getUuid()
-                            + " does not match Enrolment UUID " + mEnrolmentServiceUuid.getUuid());
-                }
-                return;
-            }
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Enrolment Service # characteristics: "
-                        + service.getCharacteristics().size());
-            }
-
-            mEnrolmentEscrowToken = BluetoothUtils.getCharacteristic(
-                    R.string.enrollment_token_uuid, service, mContext);
-            mEnrolmentTokenHandle = BluetoothUtils.getCharacteristic(
-                    R.string.enrollment_handle_uuid, service, mContext);
-            mClient.setCharacteristicNotification(mEnrolmentTokenHandle, true /* enable */);
-            appendOutputText("Enrolment BLE client successfully connected");
-
-            mHandler.post(() -> {
-                // Services are now set up, allow users to enrol new escrow tokens.
-                mEnrolButton.setEnabled(true);
-                mEnrolButton.setAlpha(1.0f);
-            });
-        }
-
-        private long convertToLong(byte[] bytes) {
-            ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
-            buffer.put(bytes);
-            buffer.flip();
-            return buffer.getLong();
-        }
-    };
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java
deleted file mode 100644
index 1296529..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java
+++ /dev/null
@@ -1,161 +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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.preference.PreferenceManager;
-import android.util.Base64;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-
-import java.nio.ByteBuffer;
-import java.util.UUID;
-
-/**
- * A controller that sets up a {@link SimpleBleClient} to connect to the BLE unlock service.
- */
-public class PhoneUnlockController {
-    private static final String TAG = "PhoneUnlockController";
-
-    private final String mTokenHandleKey;
-    private final String mEscrowTokenKey;
-
-    // BLE characteristics associated with the enrolment/add escrow token service.
-    private BluetoothGattCharacteristic mUnlockTokenHandle;
-    private BluetoothGattCharacteristic mUnlockEscrowToken;
-
-    private final ParcelUuid mUnlockServiceUuid;
-
-    private final SimpleBleClient mClient;
-    private final Context mContext;
-    private final Handler mHandler;
-
-    private TextView mTextView;
-    private Button mUnlockButton;
-
-    public PhoneUnlockController(Context context) {
-        mContext = context;
-
-        mTokenHandleKey = context.getString(R.string.pref_key_token_handle);
-        mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token);
-
-        mClient = new SimpleBleClient(context);
-        mUnlockServiceUuid = new ParcelUuid(
-                UUID.fromString(mContext.getString(R.string.unlock_service_uuid)));
-        mClient.addCallback(mCallback /* callback */);
-
-        mHandler = new Handler(mContext.getMainLooper());
-    }
-
-    /**
-     * Binds the views to the actions that can be performed by this controller.
-     *
-     * @param textView    A text view used to display results from various BLE actions
-     * @param scanButton  Button used to start scanning for available BLE devices.
-     * @param enrolButton Button used to send new escrow token to remote device.
-     */
-    public void bind(TextView textView, Button scanButton, Button enrolButton) {
-        mTextView = textView;
-        mUnlockButton = enrolButton;
-
-        scanButton.setOnClickListener(v -> mClient.start(mUnlockServiceUuid));
-
-        mUnlockButton.setEnabled(false);
-        mUnlockButton.setAlpha(0.3f);
-        mUnlockButton.setOnClickListener(v -> {
-            appendOutputText("Sending unlock token and handle to remote device");
-            sendUnlockRequest();
-        });
-    }
-
-    private void sendUnlockRequest() {
-        // Retrieve stored token and handle and write to remote device.
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
-        long handle = prefs.getLong(mTokenHandleKey, -1);
-        byte[] token = Base64.decode(prefs.getString(mEscrowTokenKey, null), Base64.DEFAULT);
-
-        mUnlockEscrowToken.setValue(token);
-        mUnlockTokenHandle.setValue(convertToBytes(handle));
-
-        mClient.writeCharacteristic(mUnlockEscrowToken);
-        mClient.writeCharacteristic(mUnlockTokenHandle);
-    }
-
-    private void appendOutputText(String text) {
-        mHandler.post(() -> mTextView.append("\n" + text));
-    }
-
-    private static byte[] convertToBytes(long l) {
-        ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
-        buffer.putLong(0, l);
-        return buffer.array();
-    }
-
-    private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() {
-        @Override
-        public void onDeviceConnected(BluetoothDevice device) {
-            appendOutputText("Device connected: " + device.getName()
-                    + " addr: " + device.getAddress());
-        }
-
-        @Override
-        public void onDeviceDisconnected() {
-            appendOutputText("Device disconnected");
-        }
-
-        @Override
-        public void onCharacteristicChanged(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic) {
-            // Not expecting any characteristics changes for the unlocking client.
-        }
-
-        @Override
-        public void onServiceDiscovered(BluetoothGattService service) {
-            if (!service.getUuid().equals(mUnlockServiceUuid.getUuid())) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Service UUID: " + service.getUuid()
-                            + " does not match Enrolment UUID " + mUnlockServiceUuid.getUuid());
-                }
-                return;
-            }
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Unlock Service # characteristics: "
-                        + service.getCharacteristics().size());
-            }
-
-            mUnlockEscrowToken = BluetoothUtils.getCharacteristic(
-                    R.string.unlock_escrow_token_uiid, service, mContext);
-            mUnlockTokenHandle = BluetoothUtils.getCharacteristic(
-                    R.string.unlock_handle_uiid, service, mContext);
-            appendOutputText("Unlock BLE client successfully connected");
-
-            mHandler.post(() -> {
-                // Services are now set up, allow users to enrol new escrow tokens.
-                mUnlockButton.setEnabled(true);
-                mUnlockButton.setAlpha(1.0f);
-            });
-        }
-    };
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java
deleted file mode 100644
index 3cce775..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java
+++ /dev/null
@@ -1,405 +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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A simple client that supports the scanning and connecting to available BLE devices. Should be
- * used along with {@link SimpleBleServer}.
- */
-public class SimpleBleClient {
-    private static final String TAG = "SimpleBleClient";
-    private static final long SCAN_TIME_MS = TimeUnit.SECONDS.toMillis(10);
-
-    private final Queue<BleAction> mBleActionQueue = new ConcurrentLinkedQueue<BleAction>();
-    private final List<ClientCallback> mCallbacks = new ArrayList<>();
-    private final Context mContext;
-    private final BluetoothLeScanner mScanner;
-
-    private BluetoothGatt mBtGatt;
-    private ParcelUuid mServiceUuid;
-
-    public SimpleBleClient(Context context) {
-        mContext = context;
-        BluetoothManager btManager = (BluetoothManager) mContext.getSystemService(
-                Context.BLUETOOTH_SERVICE);
-        mScanner = btManager.getAdapter().getBluetoothLeScanner();
-    }
-
-    /**
-     * Start scanning for a BLE devices with the specified service uuid.
-     *
-     * @param parcelUuid {@link ParcelUuid} used to identify the device that should be used for
-     *                   this client. This uuid should be the same as the one that is set in the
-     *                   {@link android.bluetooth.le.AdvertiseData.Builder} by the advertising
-     *                   device.
-     */
-    public void start(ParcelUuid parcelUuid) {
-        mServiceUuid = parcelUuid;
-
-        // We only want to scan for devices that have the correct uuid set in its advertise data.
-        List<ScanFilter> filters = new ArrayList<ScanFilter>();
-        ScanFilter.Builder serviceFilter = new ScanFilter.Builder();
-        serviceFilter.setServiceUuid(mServiceUuid);
-        filters.add(serviceFilter.build());
-
-        ScanSettings.Builder settings = new ScanSettings.Builder();
-        settings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Start scanning for uuid: " + mServiceUuid.getUuid());
-        }
-
-        mScanner.startScan(filters, settings.build(), mScanCallback);
-
-        Handler handler = new Handler();
-        handler.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Stopping Scanner");
-                }
-                mScanner.stopScan(mScanCallback);
-            }
-        }, SCAN_TIME_MS);
-    }
-
-    private boolean hasServiceUuid(ScanResult result) {
-        if (result.getScanRecord() == null
-                || result.getScanRecord().getServiceUuids() == null
-                || result.getScanRecord().getServiceUuids().size() == 0) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until
-     * other actions are complete.
-     *
-     * @param characteristic {@link BluetoothGattCharacteristic} to be written
-     */
-    public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
-        processAction(new BleAction(characteristic, BleAction.ACTION_WRITE));
-    }
-
-    /**
-     * Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until
-     * other actions are complete.
-     *
-     * @param characteristic {@link BluetoothGattCharacteristic} to be read.
-     */
-    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
-        processAction(new BleAction(characteristic, BleAction.ACTION_READ));
-    }
-
-    /**
-     * Enable or disable notification for specified {@link BluetoothGattCharacteristic}.
-     *
-     * @param characteristic The {@link BluetoothGattCharacteristic} for which to enable
-     *                       notifications.
-     * @param enabled        True if notifications should be enabled, false otherwise.
-     */
-    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
-            boolean enabled) {
-        mBtGatt.setCharacteristicNotification(characteristic, enabled);
-    }
-
-    /**
-     * Add a {@link ClientCallback} to listen for updates from BLE components
-     */
-    public void addCallback(ClientCallback callback) {
-        mCallbacks.add(callback);
-    }
-
-    public void removeCallback(ClientCallback callback) {
-        mCallbacks.remove(callback);
-    }
-
-    private void processAction(BleAction action) {
-        // Only execute actions if the queue is empty.
-        if (mBleActionQueue.size() > 0) {
-            mBleActionQueue.add(action);
-            return;
-        }
-
-        mBleActionQueue.add(action);
-        executeAction(mBleActionQueue.peek());
-    }
-
-    private void processNextAction() {
-        mBleActionQueue.poll();
-        executeAction(mBleActionQueue.peek());
-    }
-
-    private void executeAction(@Nullable BleAction action) {
-        if (action == null) {
-            return;
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Executing BLE Action type: " + action.getAction());
-        }
-
-        switch (action.getAction()) {
-            case BleAction.ACTION_WRITE:
-                mBtGatt.writeCharacteristic(action.getCharacteristic());
-                break;
-            case BleAction.ACTION_READ:
-                mBtGatt.readCharacteristic(action.getCharacteristic());
-                break;
-            default:
-                Log.e(TAG, "Encountered unknown BlueAction: " + action.getAction());
-        }
-    }
-
-    private String getStatus(int status) {
-        switch (status) {
-            case BluetoothGatt.GATT_FAILURE:
-                return "Failure";
-            case BluetoothGatt.GATT_SUCCESS:
-                return "GATT_SUCCESS";
-            case BluetoothGatt.GATT_READ_NOT_PERMITTED:
-                return "GATT_READ_NOT_PERMITTED";
-            case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
-                return "GATT_WRITE_NOT_PERMITTED";
-            case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
-                return "GATT_INSUFFICIENT_AUTHENTICATION";
-            case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
-                return "GATT_REQUEST_NOT_SUPPORTED";
-            case BluetoothGatt.GATT_INVALID_OFFSET:
-                return "GATT_INVALID_OFFSET";
-            case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
-                return "GATT_INVALID_ATTRIBUTE_LENGTH";
-            case BluetoothGatt.GATT_CONNECTION_CONGESTED:
-                return "GATT_CONNECTION_CONGESTED";
-            default:
-                return "unknown";
-        }
-    }
-
-    private ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            BluetoothDevice device = result.getDevice();
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Scan result found: " + result.getScanRecord().getServiceUuids());
-            }
-
-            if (!hasServiceUuid(result)) {
-                return;
-            }
-
-            for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Scan result UUID: " + uuid);
-                }
-
-                if (uuid.equals(mServiceUuid)) {
-                    // This client only supports connecting to one service.
-                    // Once we find one, stop scanning and open a GATT connection to the device.
-                    mScanner.stopScan(mScanCallback);
-                    mBtGatt = device.connectGatt(mContext, /* autoConnect= */ false, mGattCallback);
-                    return;
-                }
-            }
-        }
-
-        @Override
-        public void onBatchScanResults(List<ScanResult> results) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                for (ScanResult r : results) {
-                    Log.d(TAG, "Batch scanResult: " + r.getDevice().getName()
-                            + " " + r.getDevice().getAddress());
-                }
-            }
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            Log.e(TAG, "Scan failed: " + errorCode);
-        }
-    };
-
-    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
-            super.onConnectionStateChange(gatt, status, newState);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Gatt connection status: " + getStatus(status)
-                        + " newState: " + newState);
-            }
-
-            switch (newState) {
-                case BluetoothProfile.STATE_CONNECTED:
-                    mBtGatt.discoverServices();
-                    for (ClientCallback callback : mCallbacks) {
-                        callback.onDeviceConnected(gatt.getDevice());
-                    }
-                    break;
-
-                case BluetoothProfile.STATE_DISCONNECTED:
-                    for (ClientCallback callback : mCallbacks) {
-                        callback.onDeviceDisconnected();
-                    }
-                    break;
-
-                default:
-                    // Do nothing.
-            }
-        }
-
-        @Override
-        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
-            super.onServicesDiscovered(gatt, status);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onServicesDiscovered: " + status);
-            }
-
-            List<BluetoothGattService> services = gatt.getServices();
-            if (services == null || services.size() <= 0) {
-                return;
-            }
-
-            // Notify clients of newly discovered services.
-            for (BluetoothGattService service : mBtGatt.getServices()) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Found service: " + service.getUuid() + " notifying clients");
-                }
-
-                for (ClientCallback callback : mCallbacks) {
-                    callback.onServiceDiscovered(service);
-                }
-            }
-        }
-
-        @Override
-        public void onCharacteristicWrite(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onCharacteristicWrite: " + status);
-            }
-
-            processNextAction();
-        }
-
-        @Override
-        public void onCharacteristicRead(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onCharacteristicRead:" + new String(characteristic.getValue()));
-            }
-
-            processNextAction();
-        }
-
-        @Override
-        public void onCharacteristicChanged(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic) {
-            for (ClientCallback callback : mCallbacks) {
-                callback.onCharacteristicChanged(gatt, characteristic);
-            }
-            processNextAction();
-        }
-    };
-
-    /**
-     * Wrapper class to allow queuing of BLE actions. The BLE stack allows only one action to be
-     * executed at a time.
-     */
-    private static class BleAction {
-        public static final int ACTION_WRITE = 0;
-        public static final int ACTION_READ = 1;
-
-        @IntDef({ ACTION_WRITE, ACTION_READ })
-        public @interface ActionType {}
-
-        private final int mAction;
-        private final BluetoothGattCharacteristic mCharacteristic;
-
-        BleAction(BluetoothGattCharacteristic characteristic, @ActionType int action) {
-            mAction = action;
-            mCharacteristic = characteristic;
-        }
-
-        @ActionType
-        public int getAction() {
-            return mAction;
-        }
-
-        public BluetoothGattCharacteristic getCharacteristic() {
-            return mCharacteristic;
-        }
-    }
-
-    /**
-     * Callback for classes that wish to be notified of BLE updates.
-     */
-    public interface ClientCallback {
-        /**
-         * Called when a device that has a matching service UUID is found.
-         **/
-        void onDeviceConnected(BluetoothDevice device);
-
-        /** Called when the currently connected device has been disconnected. */
-        void onDeviceDisconnected();
-
-        /**
-         * Called when a characteristic has been changed.
-         *
-         * @param gatt The GATT client the characteristic is associated with.
-         * @param characteristic The characteristic that has been changed.
-         */
-        void onCharacteristicChanged(BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic);
-
-        /**
-         * Called for each {@link BluetoothGattService} that is discovered on the
-         * {@link BluetoothDevice} after a matching scan result and connection.
-         *
-         * @param service {@link BluetoothGattService} that has been discovered.
-         */
-        void onServiceDiscovered(BluetoothGattService service);
-    }
-}
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 8997e32..5612807 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -116,5 +116,26 @@
                   android:grantUriPermissions="true"
                   android:exported="true" />
 
+        <activity android:name=".AlwaysCrashingActivity"
+                  android:label="@string/always_crashing_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".NoCrashActivity"
+                  android:label="@string/no_crash_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".EmptyActivity"
+                  android:label="@string/empty_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
new file mode 100644
index 0000000..5312fee
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/empty_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/empty_activity"
+        android:layout_weight="1" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 6575ce1..0d56369 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -332,4 +332,9 @@
     <string name="usernotice" translatable="false">This screen is for showing initial user notice and is not for product. Plz change config_userNoticeUiService in CarService before shipping.</string>
     <string name="dismiss_now" translatable="false">Dismiss for now</string>
     <string name="dismiss_forever" translatable="false">Do not show again</string>
+
+    <!-- [AlwaysCrashing|NoCrash|Empty]Activity -->
+    <string name="always_crashing_activity" translatable="false">Always Crash Activity</string>
+    <string name="no_crash_activity" translatable="false">No Crash Activity</string>
+    <string name="empty_activity" translatable="false">Empty Activity</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
new file mode 100644
index 0000000..05529b2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
@@ -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 com.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Activity for testing purpose. This one always crashes inside onCreate.
+ */
+public class AlwaysCrashingActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        throw new RuntimeException("Intended crash for testing");
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
new file mode 100644
index 0000000..aad25cb
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class EmptyActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.empty_activity);
+    }
+
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
new file mode 100644
index 0000000..f10e1db
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class NoCrashActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.empty_activity);
+        TextView text = findViewById(R.id.empty_text);
+        text.setText(R.string.no_crash_activity);
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
index b03e320..84b6bcf 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
@@ -21,9 +21,7 @@
 import android.car.CarAppFocusManager.OnAppFocusChangedListener;
 import android.car.CarAppFocusManager.OnAppFocusOwnershipCallback;
 import android.car.media.CarAudioManager;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.hardware.display.DisplayManager;
@@ -34,7 +32,6 @@
 import android.media.HwAudioSource;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.util.Log;
 import android.view.Display;
@@ -124,41 +121,39 @@
     private void connectCar() {
         mContext = getContext();
         mHandler = new Handler(Looper.getMainLooper());
-        mCar = Car.createCar(mContext, new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                mAppFocusManager =
-                        (CarAppFocusManager) mCar.getCarManager(Car.APP_FOCUS_SERVICE);
-                OnAppFocusChangedListener listener = new OnAppFocusChangedListener() {
-                    @Override
-                    public void onAppFocusChanged(int appType, boolean active) {
+        mCar = Car.createCar(mContext, /* handler= */ null,
+                Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car, ready) -> {
+                    if (!ready) {
+                        return;
                     }
-                };
-                mAppFocusManager.addFocusListener(listener,
-                        CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
-                mAppFocusManager.addFocusListener(listener,
-                        CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
+                    mAppFocusManager =
+                            (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
+                    OnAppFocusChangedListener listener = new OnAppFocusChangedListener() {
+                        @Override
+                        public void onAppFocusChanged(int appType, boolean active) {
+                        }
+                    };
+                    mAppFocusManager.addFocusListener(listener,
+                            CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+                    mAppFocusManager.addFocusListener(listener,
+                            CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
 
-                mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+                    mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
 
-                //take care of zone selection
-                int[] zoneList = mCarAudioManager.getAudioZoneIds();
-                Integer[] zoneArray = Arrays.stream(zoneList).boxed().toArray(Integer[]::new);
-                mZoneAdapter = new ArrayAdapter<>(mContext,
-                        android.R.layout.simple_spinner_item, zoneArray);
-                mZoneAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-                mZoneSpinner.setAdapter(mZoneAdapter);
-                mZoneSpinner.setEnabled(true);
+                    //take care of zone selection
+                    int[] zoneList = mCarAudioManager.getAudioZoneIds();
+                    Integer[] zoneArray = Arrays.stream(zoneList).boxed().toArray(Integer[]::new);
+                    mZoneAdapter = new ArrayAdapter<>(mContext,
+                            android.R.layout.simple_spinner_item, zoneArray);
+                    mZoneAdapter.setDropDownViewResource(
+                            android.R.layout.simple_spinner_dropdown_item);
+                    mZoneSpinner.setAdapter(mZoneAdapter);
+                    mZoneSpinner.setEnabled(true);
 
-                if (mCarAudioManager.isDynamicRoutingEnabled()) {
-                    setUpDisplayPlayer();
-                }
-            }
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-            }
-            });
-        mCar.connect();
+                    if (mCarAudioManager.isDynamicRoutingEnabled()) {
+                        setUpDisplayPlayer();
+                    }
+                });
     }
 
     private void initializePlayers() {
@@ -217,9 +212,16 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
         Log.i(TAG, "onCreateView");
+        View view = inflater.inflate(R.layout.audio, container, false);
+        //Zone Spinner
+        setUpZoneSpinnerView(view);
+
+        //Display layout
+        setUpDisplayLayoutView(view);
+
         connectCar();
         initializePlayers();
-        View view = inflater.inflate(R.layout.audio, container, false);
+
         mAudioManager = (AudioManager) mContext.getSystemService(
                 Context.AUDIO_SERVICE);
         mAudioFocusHandler = new FocusHandler(
@@ -331,35 +333,6 @@
             }
         });
 
-        //Zone Spinner
-        mZoneSpinner = view.findViewById(R.id.zone_spinner);
-        mZoneSpinner.setEnabled(false);
-        mZoneSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
-                handleZoneSelection();
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> parent) {
-            }
-        });
-
-
-        mDisplayLayout = view.findViewById(R.id.audio_display_layout);
-
-        mDisplaySpinner = view.findViewById(R.id.display_spinner);
-        mDisplaySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
-                handleDisplaySelection();
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> parent) {
-            }
-        });
-
         // Manage buttons for audio player for displays
         view.findViewById(R.id.button_display_media_play_start).setOnClickListener(v -> {
             startDisplayAudio();
@@ -375,6 +348,37 @@
         return view;
     }
 
+    private void setUpDisplayLayoutView(View view) {
+        mDisplayLayout = view.findViewById(R.id.audio_display_layout);
+
+        mDisplaySpinner = view.findViewById(R.id.display_spinner);
+        mDisplaySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+                handleDisplaySelection();
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+            }
+        });
+    }
+
+    private void setUpZoneSpinnerView(View view) {
+        mZoneSpinner = view.findViewById(R.id.zone_spinner);
+        mZoneSpinner.setEnabled(false);
+        mZoneSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+                handleZoneSelection();
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+            }
+        });
+    }
+
     public void handleZoneSelection() {
         int position = mZoneSpinner.getSelectedItemPosition();
         int zone = mZoneAdapter.getItem(position);
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index 7657c38..8886913 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -17,6 +17,7 @@
 
 import android.annotation.Nullable;
 import android.car.Car;
+import android.car.Car.CarServiceLifecycleListener;
 import android.car.CarAppFocusManager;
 import android.car.CarNotConnectedException;
 import android.car.cluster.navigation.NavigationState;
@@ -33,11 +34,8 @@
 import android.car.cluster.navigation.NavigationState.Step;
 import android.car.cluster.navigation.NavigationState.Timestamp;
 import android.car.navigation.CarNavigationStatusManager;
-import android.content.ComponentName;
-import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -73,19 +71,19 @@
     private NavigationStateProto[] mNavStateData;
     private Button mTurnByTurnButton;
 
-    private ServiceConnection mCarServiceConnection = new ServiceConnection() {
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            Log.d(TAG, "Connected to Car Service");
-            mCarNavigationStatusManager = (CarNavigationStatusManager) mCarApi
-                    .getCarManager(Car.CAR_NAVIGATION_SERVICE);
-            mCarAppFocusManager = (CarAppFocusManager) mCarApi
-                    .getCarManager(Car.APP_FOCUS_SERVICE);
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
+    private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
+        if (!ready) {
             Log.d(TAG, "Disconnect from Car Service");
+            return;
+        }
+        Log.d(TAG, "Connected to Car Service");
+        try {
+            mCarNavigationStatusManager = (CarNavigationStatusManager) car.getCarManager(
+                    Car.CAR_NAVIGATION_SERVICE);
+            mCarAppFocusManager = (CarAppFocusManager) car.getCarManager(
+                    Car.APP_FOCUS_SERVICE);
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Car is not connected!", e);
         }
     };
 
@@ -117,13 +115,8 @@
 
 
     private void initCarApi() {
-        if (mCarApi != null && mCarApi.isConnected()) {
-            mCarApi.disconnect();
-            mCarApi = null;
-        }
-
-        mCarApi = Car.createCar(getContext(), mCarServiceConnection);
-        mCarApi.connect();
+        mCarApi = Car.createCar(getContext(), /* handler= */ null,
+                Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
     }
 
     @NonNull
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
index 73e3798..df9aa7b 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -16,14 +16,12 @@
 package com.google.android.car.kitchensink.volume;
 
 import android.car.Car;
+import android.car.Car.CarServiceLifecycleListener;
 import android.car.media.CarAudioManager;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.ServiceConnection;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Message;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -112,20 +110,15 @@
         public boolean mHasFocus;
     }
 
-    private final ServiceConnection mCarConnectionCallback =
-            new ServiceConnection() {
-                @Override
-                public void onServiceConnected(ComponentName name, IBinder binder) {
-                    Log.d(TAG, "Connected to Car Service");
-                    mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
-                    initVolumeInfo();
-                }
-
-                @Override
-                public void onServiceDisconnected(ComponentName name) {
-                    Log.d(TAG, "Disconnect from Car Service");
-                }
-            };
+    private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
+        if (!ready) {
+            Log.d(TAG, "Disconnect from Car Service");
+            return;
+        }
+        Log.d(TAG, "Connected to Car Service");
+        mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+        initVolumeInfo();
+    };
 
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -161,8 +154,8 @@
         mBalance = v.findViewById(R.id.balance_bar);
         mBalance.setOnSeekBarChangeListener(seekListener);
 
-        mCar = Car.createCar(getActivity(), mCarConnectionCallback);
-        mCar.connect();
+        mCar = Car.createCar(getActivity(), /* handler= */ null,
+                Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
         return v;
     }
 
@@ -210,12 +203,4 @@
         }
         mAdapter.refreshVolumes(mVolumeInfos);
     }
-
-    @Override
-    public void onDestroy() {
-        if (mCar != null) {
-            mCar.disconnect();
-        }
-        super.onDestroy();
-    }
 }
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
index aadb6f9..0aa89f9 100644
--- a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -27,14 +27,18 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.car.VehiclePropertyIds;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarUxRestrictions;
 import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.CarUxRestrictionsConfiguration.Builder;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
 import android.car.hardware.CarPropertyValue;
+import android.car.hardware.property.CarPropertyEvent;
 import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.JsonReader;
 import android.util.JsonWriter;
@@ -46,6 +50,7 @@
 import com.android.car.systeminterface.SystemInterface;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -62,6 +67,7 @@
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -220,6 +226,198 @@
         assertTrue(restrictions.toString(), expected.isSameRestrictions(restrictions));
     }
 
+    // This test only involves calling a few methods and should finish very quickly. If it doesn't
+    // finish in 20s, we probably encountered a deadlock.
+    @Test(timeout = 20000)
+    public void testInitService_NoDeadlockWithCarDrivingStateService()
+            throws Exception {
+
+        CarDrivingStateService drivingStateService = new CarDrivingStateService(mSpyContext,
+                mMockCarPropertyService);
+        CarUxRestrictionsManagerService uxRestrictionsService = new CarUxRestrictionsManagerService(
+                mSpyContext, drivingStateService, mMockCarPropertyService);
+
+        CountDownLatch dispatchingStartedSignal = new CountDownLatch(1);
+        CountDownLatch initCompleteSignal = new CountDownLatch(1);
+
+        // A deadlock can exist when the dispatching of a listener is synchronized. For instance,
+        // the CarUxRestrictionsManagerService#init() method registers a callback like this one. The
+        // deadlock risk occurs if:
+        // 1. CarUxRestrictionsManagerService has registered a listener with CarDrivingStateService
+        // 2. A synchronized method of CarUxRestrictionsManagerService starts to run
+        // 3. While the method from (2) is running, a property event occurs on a different thread
+        //    that triggers a drive state event in CarDrivingStateService. If CarDrivingStateService
+        //    handles the property event in a synchronized method, then CarDrivingStateService is
+        //    locked. The listener from (1) will wait until the lock on
+        //    CarUxRestrictionsManagerService is released.
+        // 4. The synchronized method from (2) attempts to access CarDrivingStateService. For
+        //    example, the implementation below attempts to read the restriction mode.
+        //
+        // In the above steps, both CarUxRestrictionsManagerService and CarDrivingStateService are
+        // locked and waiting on each other, hence the deadlock.
+        drivingStateService.registerDrivingStateChangeListener(
+                new ICarDrivingStateChangeListener.Stub() {
+                    @Override
+                    public void onDrivingStateChanged(CarDrivingStateEvent event)
+                            throws RemoteException {
+                        // EVENT 2 [new thread]: this callback is called from within
+                        // handlePropertyEvent(), which might (but shouldn't) lock
+                        // CarDrivingStateService
+
+                        // Notify that the dispatching process has started
+                        dispatchingStartedSignal.countDown();
+
+                        try {
+                            // EVENT 3b [new thread]: Wait until init() has finished. If these
+                            // threads don't have lock dependencies, there is no reason there
+                            // would be an issue with waiting.
+                            //
+                            // In the real world, this wait could represent a long-running
+                            // task, or hitting the below line that attempts to access the
+                            // CarUxRestrictionsManagerService (which might be locked while init
+                            // () is running).
+                            //
+                            // If there is a deadlock while waiting for init to complete, we will
+                            // never progress past this line.
+                            initCompleteSignal.await();
+                        } catch (InterruptedException e) {
+                            Assert.fail("onDrivingStateChanged thread interrupted");
+                        }
+
+                        // Attempt to access CarUxRestrictionsManagerService. If
+                        // CarUxRestrictionsManagerService is locked because it is doing its own
+                        // work, then this will wait.
+                        //
+                        // This line won't execute in the deadlock flow. However, it is an example
+                        // of a real-world piece of code that would serve the same role as the above
+                        uxRestrictionsService.getCurrentUxRestrictions();
+                    }
+                });
+
+        // EVENT 1 [new thread]: handlePropertyEvent() is called, which locks CarDrivingStateService
+        // Ideally CarPropertyService would trigger the change event, but since that is mocked
+        // we manually trigger the event. This event is what eventually triggers the dispatch to
+        // ICarDrivingStateChangeListener that was defined above.
+        Runnable propertyChangeEventRunnable =
+                () -> drivingStateService.handlePropertyEvent(
+                        new CarPropertyEvent(CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE,
+                                new CarPropertyValue<>(
+                                        VehiclePropertyIds.PERF_VEHICLE_SPEED, 0, 100f)));
+        Thread thread = new Thread(propertyChangeEventRunnable);
+        thread.start();
+
+        // Wait until propertyChangeEventRunnable has triggered and the
+        // ICarDrivingStateChangeListener callback declared above started to run.
+        dispatchingStartedSignal.await();
+
+        // EVENT 3a [main thread]: init() is called, which locks CarUxRestrictionsManagerService
+        // If init() is synchronized, thereby locking CarUxRestrictionsManagerService, and it
+        // internally attempts to access CarDrivingStateService, and if CarDrivingStateService has
+        // been locked because of the above listener, then both classes are locked and waiting on
+        // each other, so we would encounter a deadlock.
+        uxRestrictionsService.init();
+
+        // If there is a deadlock in init(), then this will never be called
+        initCompleteSignal.countDown();
+
+        // wait for thread to join to leave in a deterministic state
+        try {
+            thread.join(5000);
+        } catch (InterruptedException e) {
+            Assert.fail("Thread failed to join");
+        }
+    }
+
+    // This test only involves calling a few methods and should finish very quickly. If it doesn't
+    // finish in 20s, we probably encountered a deadlock.
+    @Test(timeout = 20000)
+    public void testSetUxRChangeBroadcastEnabled_NoDeadlockWithCarDrivingStateService()
+            throws Exception {
+
+        CarDrivingStateService drivingStateService = new CarDrivingStateService(mSpyContext,
+                mMockCarPropertyService);
+        CarUxRestrictionsManagerService uxRestrictionService = new CarUxRestrictionsManagerService(
+                mSpyContext, drivingStateService, mMockCarPropertyService);
+
+        CountDownLatch dispatchingStartedSignal = new CountDownLatch(1);
+        CountDownLatch initCompleteSignal = new CountDownLatch(1);
+
+        // See testInitService_NoDeadlockWithCarDrivingStateService for details on why a deadlock
+        // may occur. This test could fail for the same reason, except the callback we register here
+        // is purely to introduce a delay, and the deadlock actually happens inside the callback
+        // that CarUxRestrictionsManagerService#init() registers internally.
+        drivingStateService.registerDrivingStateChangeListener(
+                new ICarDrivingStateChangeListener.Stub() {
+                    @Override
+                    public void onDrivingStateChanged(CarDrivingStateEvent event)
+                            throws RemoteException {
+                        // EVENT 2 [new thread]: this callback is called from within
+                        // handlePropertyEvent(), which might (but shouldn't) lock
+                        // CarDrivingStateService
+
+                        // Notify that the dispatching process has started
+                        dispatchingStartedSignal.countDown();
+
+                        try {
+                            // EVENT 3b [new thread]: Wait until init() has finished. If these
+                            // threads don't have lock dependencies, there is no reason there
+                            // would be an issue with waiting.
+                            //
+                            // In the real world, this wait could represent a long-running
+                            // task, or hitting the line inside
+                            // CarUxRestrictionsManagerService#init()'s internal registration
+                            // that attempts to access the CarUxRestrictionsManagerService (which
+                            // might be locked while init() is running).
+                            //
+                            // If there is a deadlock while waiting for init to complete, we will
+                            // never progress past this line.
+                            initCompleteSignal.await();
+                        } catch (InterruptedException e) {
+                            Assert.fail("onDrivingStateChanged thread interrupted");
+                        }
+                    }
+                });
+
+        // The init() method internally registers a callback to CarDrivingStateService
+        uxRestrictionService.init();
+
+        // EVENT 1 [new thread]: handlePropertyEvent() is called, which locks CarDrivingStateService
+        // Ideally CarPropertyService would trigger the change event, but since that is mocked
+        // we manually trigger the event. This event eventually triggers the dispatch to
+        // ICarDrivingStateChangeListener that was defined above and a dispatch to the registration
+        // that CarUxRestrictionsManagerService internally made to CarDrivingStateService in
+        // CarUxRestrictionsManagerService#init().
+        Runnable propertyChangeEventRunnable =
+                () -> drivingStateService.handlePropertyEvent(
+                        new CarPropertyEvent(CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE,
+                                new CarPropertyValue<>(
+                                        VehiclePropertyIds.PERF_VEHICLE_SPEED, 0, 100f)));
+        Thread thread = new Thread(propertyChangeEventRunnable);
+        thread.start();
+
+        // Wait until propertyChangeEventRunnable has triggered and the
+        // ICarDrivingStateChangeListener callback declared above started to run.
+        dispatchingStartedSignal.await();
+
+        // EVENT 3a [main thread]: a synchronized method is called, which locks
+        // CarUxRestrictionsManagerService
+        //
+        // Any synchronized method that internally accesses CarDrivingStateService could encounter a
+        // deadlock if the above setup locks CarDrivingStateService.
+        uxRestrictionService.setUxRChangeBroadcastEnabled(true);
+
+        // If there is a deadlock in init(), then this will never be called
+        initCompleteSignal.countDown();
+
+        // wait for thread to join to leave in a deterministic state
+        try {
+            thread.join(5000);
+        } catch (InterruptedException e) {
+            Assert.fail("Thread failed to join");
+        }
+    }
+
+
     private CarUxRestrictionsConfiguration createEmptyConfig() {
         return createEmptyConfig(null);
     }
diff --git a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
index 894c402..69eba19 100644
--- a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
+++ b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
@@ -56,6 +56,7 @@
 import com.android.car.vehiclehal.test.MockedVehicleHal.StaticPropertyHandler;
 import com.android.car.vehiclehal.test.MockedVehicleHal.VehicleHalPropertyHandler;
 import com.android.car.vehiclehal.test.VehiclePropConfigBuilder;
+import com.android.car.vms.VmsClientManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -185,6 +186,10 @@
         return (CarPackageManagerService) mCarImpl.getCarService(Car.PACKAGE_SERVICE);
     }
 
+    public VmsClientManager getVmsClientManager() {
+        return (VmsClientManager) mCarImpl.getCarInternalService(ICarImpl.INTERNAL_VMS_MANAGER);
+    }
+
     protected Context getCarServiceContext() {
         return getContext();
     }
@@ -261,11 +266,10 @@
         if (mRealCarServiceReleased) {
             return;  // We just want to release it once.
         }
-
         mRealCarServiceReleased = true;  // To make sure it was called once.
 
         Object waitForConnection = new Object();
-        android.car.Car car = android.car.Car.createCar(context, new ServiceConnection() {
+        Car car = android.car.Car.createCar(context, new ServiceConnection() {
             @Override
             public void onServiceConnected(ComponentName name, IBinder service) {
                 synchronized (waitForConnection) {
@@ -287,10 +291,10 @@
         if (car.isConnected()) {
             Log.i(TAG, "Connected to real car service");
             CarTestManagerBinderWrapper binderWrapper =
-                    (CarTestManagerBinderWrapper) car.getCarManager(android.car.Car.TEST_SERVICE);
+                    (CarTestManagerBinderWrapper) car.getCarManager(Car.TEST_SERVICE);
             assertNotNull(binderWrapper);
 
-            CarTestManager mgr = new CarTestManager(binderWrapper.binder);
+            CarTestManager mgr = new CarTestManager(car, binderWrapper.binder);
             mgr.stopCarService(mCarServiceToken);
         }
     }
diff --git a/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java b/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
index 450b9e9..ccc2e6c 100644
--- a/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
+++ b/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.app.ActivityManager;
 import android.car.Car;
 import android.car.VehicleAreaType;
 import android.car.vms.VmsAvailableLayers;
@@ -25,7 +26,6 @@
 import android.car.vms.VmsPublisherClientService;
 import android.car.vms.VmsSubscriberManager;
 import android.car.vms.VmsSubscriptionState;
-import android.content.Intent;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
@@ -34,7 +34,6 @@
 import android.hardware.automotive.vehicle.V2_0.VmsBaseMessageIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
 import android.hardware.automotive.vehicle.V2_0.VmsStartSessionMessageIntegerValuesIndex;
-import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
 
@@ -83,8 +82,7 @@
     @Before
     public void setUpVms() throws Exception {
         // Trigger VmsClientManager to bind to the MockPublisherClient
-        getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_UNLOCKED), UserHandle.ALL);
-
+        getVmsClientManager().mUserCallback.onSwitchUser(ActivityManager.getCurrentUser());
         mVmsSubscriberManager = (VmsSubscriberManager) getCar().getCarManager(
                 Car.VMS_SUBSCRIBER_SERVICE);
         mSubscriberClient = new MockSubscriberClient();
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
index 0e3adde..0bb9d4f 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
@@ -21,10 +21,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.app.ActivityManager;
 import android.car.vms.VmsPublisherClientService;
 import android.car.vms.VmsSubscriptionState;
-import android.content.Intent;
-import android.os.UserHandle;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -98,7 +97,7 @@
 
     @Before
     public void triggerClientBinding() {
-        getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_UNLOCKED), UserHandle.ALL);
+        getVmsClientManager().mUserCallback.onSwitchUser(ActivityManager.getCurrentUser());
     }
 
     @Test
diff --git a/tests/carservice_unit_test/AndroidManifest.xml b/tests/carservice_unit_test/AndroidManifest.xml
index e5e31bf..5ed59ef 100644
--- a/tests/carservice_unit_test/AndroidManifest.xml
+++ b/tests/carservice_unit_test/AndroidManifest.xml
@@ -21,6 +21,7 @@
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.car.carservice_unittest"
             android:label="Unit Tests for Car APIs"/>
+    <uses-permission android:name="android.car.permission.CAR_ENROLL_TRUST" />
 
     <application android:label="CarServiceUnitTest"
             android:debuggable="true">
diff --git a/tests/carservice_unit_test/src/android/car/CarTest.java b/tests/carservice_unit_test/src/android/car/CarTest.java
index 9ac8d75..d1f5ac6 100644
--- a/tests/carservice_unit_test/src/android/car/CarTest.java
+++ b/tests/carservice_unit_test/src/android/car/CarTest.java
@@ -152,7 +152,9 @@
     @Test
     public void testCreateCarSuccessWithCarServiceRunning() {
         expectService(mService);
-        assertThat(Car.createCar(mContext)).isNotNull();
+        Car car = Car.createCar(mContext);
+        assertThat(car).isNotNull();
+        car.disconnect();
     }
 
     @Test
@@ -171,14 +173,7 @@
         Car car = Car.createCar(mContext);
         assertThat(car).isNotNull();
         assertServiceBoundOnce();
-
-        // Just call these to guarantee that nothing crashes when service is connected /
-        // disconnected.
-        runOnMainSyncSafe(() -> {
-            car.getServiceConnectionListener().onServiceConnected(new ComponentName("", ""),
-                    mService);
-            car.getServiceConnectionListener().onServiceDisconnected(new ComponentName("", ""));
-        });
+        car.disconnect();
     }
 
     @Test
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 fa82f90..945a9d6 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -197,7 +197,7 @@
         // second processing after wakeup
         assertFalse(mDisplayInterface.getDisplayState());
         // do not skip user switching part.
-        mService.clearIsBooting();
+        mService.clearIsBootingOrResuming();
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
         assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
         // user switching should have been requested.
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
index 866bc8a..2704654 100644
--- a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
@@ -16,8 +16,6 @@
 
 package com.android.car;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
@@ -45,25 +43,24 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
 import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.List;
 
 @SmallTest
 public class VmsPublisherServiceTest {
@@ -72,20 +69,22 @@
     private static final VmsLayersOffering OFFERING = new VmsLayersOffering(Collections.emptySet(),
             54321);
     private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
-    private static final VmsLayer LAYER2 = new VmsLayer(2, 2, 8);
-    private static final VmsLayer LAYER3 = new VmsLayer(3, 2, 8);
-    private static final VmsLayer LAYER4 = new VmsLayer(4, 2, 8);
 
     private static final int PUBLISHER_ID = 54321;
     private static final byte[] PAYLOAD = new byte[]{1, 2, 3, 4};
-    private static final byte[] PAYLOAD2 = new byte[]{1, 2, 3, 4, 5, 6};
-    private static final byte[] PAYLOAD3 = new byte[]{10, 12, 93, 4, 5, 6, 1, 1, 1};
+
+    private static final int PUBLISHER_UID = 10100;
+    private static final int SUBSCRIBER_UID = 10101;
+    private static final int SUBSCRIBER_UID2 = 10102;
+    private static final int NO_SUBSCRIBERS_UID = -1;
 
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Mock
     private Context mContext;
     @Mock
+    private CarStatsService mStatsService;
+    @Mock
     private VmsBrokerService mBrokerService;
     @Captor
     private ArgumentCaptor<VmsBrokerService.PublisherListener> mProxyCaptor;
@@ -93,13 +92,18 @@
     private VmsClientManager mClientManager;
 
     @Mock
+    private VmsClientLog mPublisherLog;
+    @Mock
+    private VmsClientLog mSubscriberLog;
+    @Mock
+    private VmsClientLog mSubscriberLog2;
+    @Mock
+    private VmsClientLog mNoSubscribersLog;
+
+    @Mock
     private IVmsSubscriberClient mSubscriberClient;
     @Mock
     private IVmsSubscriberClient mSubscriberClient2;
-    @Mock
-    private IVmsSubscriberClient mThrowingSubscriberClient;
-    @Mock
-    private IVmsSubscriberClient mThrowingSubscriberClient2;
 
     private VmsPublisherService mPublisherService;
     private MockPublisherClient mPublisherClient;
@@ -107,14 +111,27 @@
 
     @Before
     public void setUp() {
-        mPublisherService = new VmsPublisherService(mContext, mBrokerService, mClientManager);
+        mPublisherService = new VmsPublisherService(mContext, mStatsService, mBrokerService,
+                mClientManager, () -> PUBLISHER_UID);
         verify(mClientManager).setPublisherService(mPublisherService);
 
+        when(mClientManager.getSubscriberUid(mSubscriberClient)).thenReturn(SUBSCRIBER_UID);
+        when(mClientManager.getSubscriberUid(mSubscriberClient2)).thenReturn(SUBSCRIBER_UID2);
+
+        when(mStatsService.getVmsClientLog(PUBLISHER_UID)).thenReturn(mPublisherLog);
+        when(mStatsService.getVmsClientLog(SUBSCRIBER_UID)).thenReturn(mSubscriberLog);
+        when(mStatsService.getVmsClientLog(SUBSCRIBER_UID2)).thenReturn(mSubscriberLog2);
+        when(mStatsService.getVmsClientLog(NO_SUBSCRIBERS_UID)).thenReturn(mNoSubscribersLog);
+
         mPublisherClient = new MockPublisherClient();
         mPublisherClient2 = new MockPublisherClient();
         when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
                 .thenReturn(new HashSet<>(Arrays.asList(mSubscriberClient, mSubscriberClient2)));
+    }
 
+    @After
+    public void tearDown() {
+        verifyNoMoreInteractions(mPublisherLog, mSubscriberLog, mSubscriberLog2, mNoSubscribersLog);
     }
 
     @Test
@@ -190,6 +207,10 @@
                 PAYLOAD);
         verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
         verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog).logPacketReceived(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
     }
 
     @Test
@@ -202,6 +223,19 @@
     }
 
     @Test
+    public void testPublish_NoSubscribers() throws Exception {
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
+        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
+                .thenReturn(Collections.emptySet());
+
+        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
+                PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mNoSubscribersLog).logPacketDropped(LAYER, PAYLOAD.length);
+    }
+
+    @Test
     public void testPublish_ClientError() throws Exception {
         mPublisherService.onClientConnected("SomeClient", mPublisherClient);
         doThrow(new RemoteException()).when(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
@@ -210,6 +244,10 @@
                 PAYLOAD);
         verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
         verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog).logPacketDropped(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
     }
 
     @Test(expected = SecurityException.class)
@@ -301,341 +339,6 @@
     }
 
     @Test
-    public void testDump_getPacketCount() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-        String expectedPacketCountString = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER, 1L);
-        String expectedPacketSizeString = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER, PAYLOAD.length);
-        assertThat(dumpString.contains(expectedPacketCountString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDump_getPacketCounts() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD2);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        // LAYER called 6 times with PAYLOAD 2 times, PAYLOAD2 1 time, PAYLOAD3 3 times
-        String expectedPacketCountString1 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER, 6L);
-        String expectedPacketSizeString1 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER, 2 * PAYLOAD.length + PAYLOAD2.length + 3 * PAYLOAD3.length);
-
-        // LAYER2 called 2 times with PAYLOAD 1 time, PAYLOAD2 0 time, PAYLOAD3 1 times
-        String expectedPacketCountString2 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER2, 2L);
-        String expectedPacketSizeString2 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER2, PAYLOAD.length + PAYLOAD3.length);
-
-        // LAYER3 called 2 times with PAYLOAD 2 times, PAYLOAD2 0 time, PAYLOAD3 0 times
-        String expectedPacketCountString3 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER3, 2L);
-        String expectedPacketSizeString3 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER3, 2 * PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketCountString1)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString1)).isTrue();
-        assertThat(dumpString.contains(expectedPacketCountString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketCountString3)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString3)).isTrue();
-    }
-
-    @Test
-    public void testDumpNoListeners_getPacketFailureCount() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER2, "SomeClient", "", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER2, "SomeClient", "", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDumpNoListeners_getPacketFailureCounts() throws Exception {
-        // LAYER2 and LAYER3 both have no listeners
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER2, PUBLISHER_ID))
-                .thenReturn(new HashSet<>());
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>());
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER2, "SomeClient", "", 1L);
-        String expectedPacketFailureString2 = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER3, "SomeClient2", "", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER2, "SomeClient", "", PAYLOAD.length);
-        String expectedPacketFailureSizeString2 = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER3, "SomeClient2", "", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString2)).isTrue();
-    }
-
-    @Test
-    public void testDumpRemoteException_getPacketFailureCount() throws Exception {
-        // The listener on LAYER3 will throw on LAYER3 and PAYLOAD
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER3, PAYLOAD);
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(Arrays.asList(mThrowingSubscriberClient)));
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER3, "SomeClient", "Thrower", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER3, "SomeClient", "Thrower", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDumpRemoteException_getPacketFailureCounts() throws Exception {
-        // The listeners will throw on LAYER3 or LAYER4 and PAYLOAD
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER3, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER4, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(
-                mThrowingSubscriberClient2).onVmsMessageReceived(LAYER3, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(
-                mThrowingSubscriberClient2).onVmsMessageReceived(LAYER4, PAYLOAD);
-
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
-
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-        when(mClientManager.getPackageName(mThrowingSubscriberClient2)).thenReturn("Thrower2");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        List<String> expectedStrings = Arrays.asList(
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
-                        "SomeClient2",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
-                        "SomeClient2",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
-                        "SomeClient2",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
-                        "SomeClient2",
-                        "Thrower2", 2L),
-
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
-                        "Thrower2", 2 * PAYLOAD.length));
-
-        for (String expected : expectedStrings) {
-            assertThat(dumpString.contains(expected)).isTrue();
-        }
-    }
-
-    @Test
-    public void testDump_getAllMetrics() throws Exception {
-
-        // LAYER3 has no subscribers
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(Arrays.asList()));
-
-        // LAYER4 has a subscriber that will always throw
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER4, PAYLOAD);
-
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient)));
-
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD2);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        List<String> expectedStrings = Arrays.asList(
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER, 2),
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER3, 1),
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER4, 1),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER,
-                        PAYLOAD.length + PAYLOAD2.length),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER3, PAYLOAD3.length),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER4, PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "",
-                        1),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "",
-                        PAYLOAD3.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 1),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", PAYLOAD.length)
-        );
-
-        for (String expected : expectedStrings) {
-            assertThat(dumpString.contains(expected)).isTrue();
-        }
-    }
-
-
-    @Test
     public void testRelease() {
         mPublisherService.release();
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java
new file mode 100644
index 0000000..6f7fe18
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.hal;
+
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class PropertyHalServiceTest {
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock
+    private VehicleHal mVehicleHal;
+
+    private PropertyHalService mPropertyHalService;
+    private static final int[] UNITS_PROPERTY_ID = {
+            VehicleProperty.DISTANCE_DISPLAY_UNITS,
+            VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME,
+            VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS,
+            VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS,
+            VehicleProperty.EV_BATTERY_DISPLAY_UNITS,
+            VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS};
+
+    @Before
+    public void setUp() {
+        mPropertyHalService = new PropertyHalService(mVehicleHal);
+        mPropertyHalService.init();
+    }
+
+    @After
+    public void tearDown() {
+        mPropertyHalService.release();
+        mPropertyHalService = null;
+    }
+
+    @Test
+    public void checkDisplayUnitsProperty() {
+        for (int propId : UNITS_PROPERTY_ID) {
+            Assert.assertTrue(mPropertyHalService.isDisplayUnitsProperty(propId));
+        }
+    }
+}
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 7571867..093ab9b 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
@@ -17,7 +17,9 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -44,8 +46,6 @@
 import android.os.Binder;
 import android.os.IBinder;
 
-import androidx.test.filters.RequiresDevice;
-
 import com.android.car.R;
 import com.android.car.test.utils.TemporaryFile;
 import com.android.car.vms.VmsClientManager;
@@ -103,8 +103,13 @@
 
     @Before
     public void setUp() throws Exception {
+        initHalService(true);
+    }
+
+    private void initHalService(boolean propagatePropertyException) throws Exception {
         when(mContext.getResources()).thenReturn(mResources);
-        mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID);
+        mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID,
+            propagatePropertyException);
         mHalService.setClientManager(mClientManager);
         mHalService.setVmsSubscriberService(mSubscriberService);
 
@@ -158,6 +163,8 @@
                 0,                                  // Sequence number
                 0));                                // # of associated layers
 
+
+        waitForHandlerCompletion();
         initOrder.verifyNoMoreInteractions();
         reset(mClientManager, mSubscriberService, mVehicleHal);
     }
@@ -165,7 +172,7 @@
     @Test
     public void testCoreId_IntegerOverflow() throws Exception {
         mHalService = new VmsHalService(mContext, mVehicleHal,
-                () -> (long) Integer.MAX_VALUE + CORE_ID);
+                () -> (long) Integer.MAX_VALUE + CORE_ID, true);
 
         VehiclePropConfig propConfig = new VehiclePropConfig();
         propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
@@ -587,7 +594,6 @@
      * </ul>
      */
     @Test
-    @RequiresDevice
     public void testHandleStartSessionEvent() throws Exception {
         when(mSubscriberService.getAvailableLayers()).thenReturn(
                 new VmsAvailableLayers(Collections.emptySet(), 5));
@@ -605,13 +611,18 @@
         );
 
         sendHalMessage(request);
+
         InOrder inOrder = Mockito.inOrder(mClientManager, mVehicleHal);
         inOrder.verify(mClientManager).onHalDisconnected();
         inOrder.verify(mVehicleHal).set(response);
+        inOrder.verify(mClientManager).onHalConnected(mPublisherClient, mSubscriberClient);
+
+        waitForHandlerCompletion();
         inOrder.verify(mVehicleHal).set(createHalMessage(
                 VmsMessageType.AVAILABILITY_CHANGE, // Message type
                 5,                                  // Sequence number
                 0));                                // # of associated layers
+
     }
 
     /**
@@ -962,6 +973,33 @@
     }
 
     @Test
+    public void testPropertySetExceptionNotPropagated_CoreStartSession() throws Exception {
+        doThrow(new RuntimeException()).when(mVehicleHal).set(any());
+        initHalService(false);
+
+        mHalService.init();
+        waitForHandlerCompletion();
+    }
+
+    @Test
+    public void testPropertySetExceptionNotPropagated_ClientStartSession() throws Exception {
+        initHalService(false);
+
+        when(mSubscriberService.getAvailableLayers()).thenReturn(
+                new VmsAvailableLayers(Collections.emptySet(), 0));
+        doThrow(new RuntimeException()).when(mVehicleHal).set(any());
+
+        VehiclePropValue request = createHalMessage(
+                VmsMessageType.START_SESSION,  // Message type
+                -1,                            // Core ID (unknown)
+                CLIENT_ID                      // Client ID
+        );
+
+        sendHalMessage(request);
+        waitForHandlerCompletion();
+    }
+
+    @Test
     public void testDumpMetrics_DefaultConfig() {
         mHalService.dumpMetrics(new FileDescriptor());
         verifyZeroInteractions(mVehicleHal);
diff --git a/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
new file mode 100644
index 0000000..a18c88f
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
@@ -0,0 +1,357 @@
+/*
+ * 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.stats;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.car.vms.VmsLayer;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.car.stats.VmsClientLog.ConnectionState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class CarStatsServiceTest {
+    private static final int CLIENT_UID = 10101;
+    private static final int CLIENT_UID2 = 10102;
+    private static final String CLIENT_PACKAGE = "test.package";
+    private static final String CLIENT_PACKAGE2 = "test.package2";
+    private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
+    private static final VmsLayer LAYER2 = new VmsLayer(2, 3, 4);
+    private static final VmsLayer LAYER3 = new VmsLayer(3, 4, 5);
+
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+
+    private CarStatsService mCarStatsService;
+    private StringWriter mDumpsysOutput;
+    private PrintWriter mDumpsysWriter;
+
+    @Before
+    public void setUp() {
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getNameForUid(CLIENT_UID)).thenReturn(CLIENT_PACKAGE);
+        when(mPackageManager.getNameForUid(CLIENT_UID2)).thenReturn(CLIENT_PACKAGE2);
+
+        mCarStatsService = new CarStatsService(mContext);
+        mDumpsysOutput = new StringWriter();
+        mDumpsysWriter = new PrintWriter(mDumpsysOutput);
+    }
+
+    @Test
+    public void testEmptyStats() {
+        mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+        assertEquals(
+                "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+                        + "\nuid,layerType,layerChannel,layerVersion,"
+                        + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n",
+                mDumpsysOutput.toString());
+    }
+
+    @Test
+    public void testLogConnectionState_Connecting() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        validateConnectionStats("10101,test.package,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Connected() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        validateConnectionStats("10101,test.package,0,1,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Disconnected() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        validateConnectionStats("10101,test.package,0,0,1,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Terminated() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.TERMINATED);
+        validateConnectionStats("10101,test.package,0,0,0,1,0");
+    }
+
+    @Test
+    public void testLogConnectionState_ConnectionError() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTION_ERROR);
+        validateConnectionStats("10101,test.package,0,0,0,0,1");
+    }
+
+    @Test
+    public void testLogConnectionState_UnknownUID() {
+        mCarStatsService.getVmsClientLog(-1)
+                .logConnectionState(ConnectionState.CONNECTING);
+        testEmptyStats();
+    }
+
+    @Test
+    public void testLogConnectionState_MultipleClients_MultipleStates() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.TERMINATED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTION_ERROR);
+        validateConnectionStats(
+                "10101,test.package,1,2,1,0,0\n"
+                + "10102,test.package2,2,1,0,1,1");
+    }
+
+    @Test
+    public void testLogPacketSent() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 5);
+        validateClientStats("10101,1,2,3,5,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,6,3,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,3,1,0,0,0,0\n"
+                        + "10101,2,3,4,2,1,0,0,0,0\n"
+                        + "10101,3,4,5,1,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,3,1,0,0,0,0\n"
+                        + "10102,1,2,3,2,1,0,0,0,0\n"
+                        + "10102,2,3,4,1,1,0,0,0,0\n");
+    }
+
+    @Test
+    public void testLogPacketReceived() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,5,1,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,6,3,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,0,0,3,1,0,0\n"
+                        + "10101,2,3,4,0,0,2,1,0,0\n"
+                        + "10101,3,4,5,0,0,1,1,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,0,0,3,1,0,0\n"
+                        + "10102,1,2,3,0,0,2,1,0,0\n"
+                        + "10102,2,3,4,0,0,1,1,0,0\n");
+    }
+
+    @Test
+    public void testLogPacketDropped() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,0,0,5,1");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,0,0,6,3");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,0,0,0,0,3,1\n"
+                        + "10101,2,3,4,0,0,0,0,2,1\n"
+                        + "10101,3,4,5,0,0,0,0,1,1");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketDropped(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,0,0,0,0,3,1\n"
+                        + "10102,1,2,3,0,0,0,0,2,1\n"
+                        + "10102,2,3,4,0,0,0,0,1,1\n");
+    }
+
+    @Test
+    public void testLogPackets_MultipleClients_MultipleLayers_MultipleOperations() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(-1)
+                .logPacketDropped(LAYER2, 12);
+
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "-1,2,3,4,0,0,0,0,12,1\n"
+                        + "10101,1,2,3,3,1,2,1,1,1\n"
+                        + "10102,1,2,3,0,0,6,3,0,0\n"
+                        + "10102,2,3,4,2,1,0,0,0,0\n");
+    }
+
+
+    private void validateConnectionStats(String vmsConnectionStats) {
+        validateDumpsys(vmsConnectionStats + "\n", "");
+    }
+
+    private void validateClientStats(String vmsClientStats) {
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n",
+                vmsClientStats + "\n");
+    }
+
+    private void validateDumpsys(String vmsConnectionStats, String vmsClientStats) {
+        mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+        assertEquals(
+                "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+                        + vmsConnectionStats
+                        + "\n"
+                        + "uid,layerType,layerChannel,layerVersion,"
+                        + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n"
+                        + vmsClientStats,
+                mDumpsysOutput.toString());
+    }
+}
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 b90dac4..c7d6489 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
@@ -25,7 +25,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atMost;
@@ -38,7 +37,6 @@
 import static org.mockito.Mockito.when;
 
 import android.car.Car;
-import android.car.userlib.CarUserManagerHelper;
 import android.car.vms.IVmsPublisherClient;
 import android.car.vms.IVmsPublisherService;
 import android.car.vms.IVmsSubscriberClient;
@@ -48,14 +46,15 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 
@@ -63,6 +62,9 @@
 
 import com.android.car.VmsPublisherService;
 import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
+import com.android.car.stats.VmsClientLog.ConnectionState;
 import com.android.car.user.CarUserService;
 
 import org.junit.After;
@@ -90,13 +92,18 @@
     private static final String USER_CLIENT_NAME =
             "com.google.android.apps.vms.test/com.google.android.apps.vms.test.VmsUserClient U=10";
     private static final int USER_ID_U11 = 11;
-    private static final String USER_CLIENT_NAME_U11 =
-            "com.google.android.apps.vms.test/com.google.android.apps.vms.test.VmsUserClient U=11";
 
     private static final String TEST_PACKAGE = "test.package1";
     private static final String HAL_CLIENT_NAME = "HalClient";
     private static final String UNKNOWN_PACKAGE = "UnknownPackage";
 
+    private static final int TEST_APP_ID = 12345;
+    private static final int TEST_SYSTEM_UID = 12345;
+    private static final int TEST_USER_UID = 1012345;
+    private static final int TEST_USER_UID_U11 = 1112345;
+
+    private static final long MILLIS_BEFORE_REBIND = 100;
+
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Mock
@@ -105,13 +112,12 @@
     private PackageManager mPackageManager;
     @Mock
     private Resources mResources;
-
+    @Mock
+    private CarStatsService mStatsService;
     @Mock
     private UserManager mUserManager;
     @Mock
     private CarUserService mUserService;
-    @Mock
-    private CarUserManagerHelper mUserManagerHelper;
 
     @Mock
     private VmsBrokerService mBrokerService;
@@ -120,6 +126,12 @@
     private VmsHalService mHal;
 
     @Mock
+    private Handler mHandler;
+
+    @Captor
+    private ArgumentCaptor<Runnable> mRebindCaptor;
+
+    @Mock
     private VmsPublisherService mPublisherService;
 
     @Mock
@@ -138,21 +150,48 @@
     @Captor
     private ArgumentCaptor<ServiceConnection> mConnectionCaptor;
 
+    @Mock
+    private VmsClientLog mSystemClientLog;
+    @Mock
+    private VmsClientLog mUserClientLog;
+    @Mock
+    private VmsClientLog mUserClientLog2;
+    @Mock
+    private VmsClientLog mHalClientLog;
+
     private VmsClientManager mClientManager;
 
     private int mForegroundUserId;
     private int mCallingAppUid;
 
+    private ServiceInfo mSystemServiceInfo;
+    private ServiceInfo mUserServiceInfo;
+
     @Before
     public void setUp() throws Exception {
         resetContext();
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
-        when(mPackageManager.getServiceInfo(any(), anyInt())).thenReturn(serviceInfo);
+        mSystemServiceInfo = new ServiceInfo();
+        mSystemServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+        mSystemServiceInfo.applicationInfo = new ApplicationInfo();
+        mSystemServiceInfo.applicationInfo.uid = TEST_APP_ID;
+        when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
+                .thenReturn(mSystemServiceInfo);
+        when(mStatsService.getVmsClientLog(TEST_SYSTEM_UID)).thenReturn(mSystemClientLog);
+
+        mUserServiceInfo = new ServiceInfo();
+        mUserServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+        mUserServiceInfo.applicationInfo = new ApplicationInfo();
+        mUserServiceInfo.applicationInfo.uid = TEST_APP_ID;
+        when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
+                .thenReturn(mUserServiceInfo);
+        when(mStatsService.getVmsClientLog(TEST_USER_UID)).thenReturn(mUserClientLog);
+        when(mStatsService.getVmsClientLog(TEST_USER_UID_U11)).thenReturn(mUserClientLog2);
+
+        when(mStatsService.getVmsClientLog(Process.myUid())).thenReturn(mHalClientLog);
 
         when(mResources.getInteger(
                 com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher)).thenReturn(
-                5);
+                (int) MILLIS_BEFORE_REBIND);
         when(mResources.getStringArray(
                 com.android.car.R.array.vmsPublisherSystemClients)).thenReturn(
                 new String[]{ SYSTEM_CLIENT });
@@ -161,17 +200,15 @@
                 new String[]{ USER_CLIENT });
 
         when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
-        when(mUserManagerHelper.getCurrentForegroundUserId())
-                .thenAnswer(invocation -> mForegroundUserId);
 
-        mForegroundUserId = USER_ID;
-        mCallingAppUid = UserHandle.getUid(USER_ID, 0);
-
-        mClientManager = new VmsClientManager(mContext, mBrokerService, mUserService,
-                mUserManagerHelper, mHal, () -> mCallingAppUid);
+        mClientManager = new VmsClientManager(mContext, mStatsService, mUserService,
+                mBrokerService, mHal, mHandler, () -> mCallingAppUid);
         verify(mHal).setClientManager(mClientManager);
         mClientManager.setPublisherService(mPublisherService);
 
+        notifyUserSwitched(USER_ID, false);
+        mCallingAppUid = UserHandle.getUid(USER_ID, 0);
+
         when(mSubscriberClient1.asBinder()).thenReturn(mSubscriberBinder1);
         when(mSubscriberClient2.asBinder()).thenReturn(mSubscriberBinder2);
 
@@ -179,12 +216,12 @@
     }
 
     @After
-    public void tearDown() throws Exception {
-        Thread.sleep(10); // Time to allow for delayed rebinds to settle
+    public void tearDown() {
         verify(mContext, atLeast(0)).getSystemService(eq(Context.USER_SERVICE));
         verify(mContext, atLeast(0)).getResources();
         verify(mContext, atLeast(0)).getPackageManager();
-        verifyNoMoreInteractions(mContext, mBrokerService, mHal, mPublisherService);
+        verifyNoMoreInteractions(mContext, mBrokerService, mHal, mPublisherService, mHandler);
+        verifyNoMoreInteractions(mSystemClientLog, mUserClientLog, mUserClientLog2, mHalClientLog);
     }
 
     @Test
@@ -193,15 +230,8 @@
 
         // Verify registration of system user unlock listener
         verify(mUserService).runOnUser0Unlock(mClientManager.mSystemUserUnlockedListener);
-
-        // Verify registration of user switch receiver
-        ArgumentCaptor<IntentFilter> userFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mContext).registerReceiverAsUser(eq(mClientManager.mUserSwitchReceiver),
-                eq(UserHandle.ALL), userFilterCaptor.capture(), isNull(), isNull());
-        IntentFilter userEventFilter = userFilterCaptor.getValue();
-        assertEquals(2, userEventFilter.countActions());
-        assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_SWITCHED));
-        assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_UNLOCKED));
+        // Verify user callback is added
+        verify(mUserService).addUserCallback(eq(mClientManager.mUserCallback));
     }
 
     @Test
@@ -209,7 +239,7 @@
         mClientManager.release();
 
         // Verify user switch receiver is unregistered
-        verify(mContext).unregisterReceiver(mClientManager.mUserSwitchReceiver);
+        verify(mUserService).removeUserCallback(mClientManager.mUserCallback);
     }
 
     @Test
@@ -233,14 +263,12 @@
 
     @Test
     public void testSystemUserUnlocked_WrongPermission() throws Exception {
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
-        when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
-                .thenReturn(serviceInfo);
+        mSystemServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
         notifySystemUserUnlocked();
 
         // Process will not be bound
         verifySystemBind(0);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -251,6 +279,7 @@
 
         // Failure state will trigger another attempt on event
         verifySystemBind(2);
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -262,6 +291,7 @@
 
         // Failure state will trigger another attempt on event
         verifySystemBind(2);
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -282,6 +312,14 @@
     }
 
     @Test
+    public void testUserUnlocked_OtherUserUnlocked() {
+        notifyUserUnlocked(USER_ID_U11, true);
+
+        // Process will not be bound
+        verifyUserBind(0);
+    }
+
+    @Test
     public void testUserUnlocked_ClientNotFound() throws Exception {
         when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
                 .thenThrow(new PackageManager.NameNotFoundException());
@@ -293,14 +331,12 @@
 
     @Test
     public void testUserUnlocked_WrongPermission() throws Exception {
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
-        when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
-                .thenReturn(serviceInfo);
+        mUserServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
         notifyUserUnlocked(USER_ID, true);
 
         // Process will not be bound
         verifyUserBind(0);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -312,6 +348,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -323,6 +360,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -334,6 +372,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -342,6 +381,7 @@
                 .thenReturn(false);
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -357,6 +397,7 @@
                 .thenReturn(false);
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -365,6 +406,7 @@
         notifyUserUnlocked(USER_ID, true);
 
         verifySystemBind(2); // Failure state will trigger another attempt
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
         verifyUserBind(1);
     }
 
@@ -374,6 +416,7 @@
                 .thenThrow(new SecurityException());
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -382,6 +425,7 @@
         notifyUserUnlocked(USER_ID, true);
 
         verifySystemBind(2); // Failure state will trigger another attempt
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
         verifyUserBind(1);
     }
 
@@ -424,6 +468,7 @@
     public void testOnSystemServiceConnected() {
         IBinder binder = bindSystemClient();
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
     }
 
     private IBinder bindSystemClient() {
@@ -441,6 +486,7 @@
     public void testOnUserServiceConnected() {
         IBinder binder = bindUserClient();
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
     }
 
     private IBinder bindUserClient() {
@@ -462,12 +508,14 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifySystemBind(1);
     }
@@ -482,14 +530,19 @@
         IBinder binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
-        reset(mPublisherService);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
+        reset(mPublisherService, mSystemClientLog);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
+
+        verifyAndRunRebindTask();
         // No more interactions (verified by tearDown)
     }
 
@@ -502,12 +555,14 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onBindingDied(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifySystemBind(1);
     }
@@ -523,7 +578,7 @@
 
         verifyZeroInteractions(mPublisherService);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifySystemBind(1);
     }
@@ -536,12 +591,14 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifyUserBind(1);
     }
@@ -556,14 +613,19 @@
         IBinder binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
-        reset(mPublisherService);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
+        reset(mPublisherService, mUserClientLog);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
+
+        verifyAndRunRebindTask();
         // No more interactions (verified by tearDown)
     }
 
@@ -575,12 +637,14 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onBindingDied(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifyUserBind(1);
     }
@@ -596,7 +660,7 @@
 
         verifyZeroInteractions(mPublisherService);
 
-        Thread.sleep(10);
+        verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
         verifyUserBind(1);
     }
@@ -605,15 +669,18 @@
     public void testOnUserSwitched_UserChange() {
         notifyUserUnlocked(USER_ID, true);
         verifyUserBind(1);
+        resetContext();
+
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
-        resetContext();
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         notifyUserSwitched(USER_ID_U11, true);
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(1);
     }
 
@@ -630,6 +697,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(0);
     }
 
@@ -646,6 +714,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(0);
     }
 
@@ -684,10 +753,12 @@
         resetContext();
         reset(mPublisherService);
 
+        notifyUserSwitched(USER_ID_U11, false);
         notifyUserUnlocked(USER_ID_U11, true);
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(1);
     }
 
@@ -702,10 +773,12 @@
         resetContext();
         reset(mPublisherService);
 
+        notifyUserSwitched(USER_ID_U11, false);
         notifyUserUnlocked(UserHandle.USER_SYSTEM, true);
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         // User processes will not be bound for system user
         verifyUserBind(0);
     }
@@ -717,6 +790,7 @@
         ServiceConnection connection = mConnectionCaptor.getValue();
         resetContext();
 
+        notifyUserSwitched(USER_ID_U11, false);
         notifyUserUnlocked(USER_ID_U11, true);
 
         verify(mContext).unbindService(connection);
@@ -727,7 +801,9 @@
     public void testAddSubscriber() {
         mClientManager.addSubscriber(mSubscriberClient1);
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -737,7 +813,9 @@
 
         mClientManager.addSubscriber(mSubscriberClient1);
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -751,6 +829,7 @@
             // expected
         }
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
@@ -759,7 +838,9 @@
         mClientManager.addSubscriber(mSubscriberClient1);
         verify(mPackageManager, atMost(1)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -768,11 +849,14 @@
         mClientManager.addSubscriber(mSubscriberClient2);
         verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
     public void testAddSubscriber_MultipleClients_ForegroundAndSystemUsers_SamePackage() {
+        int clientUid1 = mCallingAppUid;
         mClientManager.addSubscriber(mSubscriberClient1);
 
         mCallingAppUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 0);
@@ -781,12 +865,15 @@
 
         verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
 
     @Test
     public void testAddSubscriber_MultipleClients_MultiplePackages() {
+        int clientUid1 = mCallingAppUid;
         mClientManager.addSubscriber(mSubscriberClient1);
 
         mCallingAppUid = UserHandle.getUid(mForegroundUserId, 1);
@@ -795,7 +882,9 @@
 
         verify(mPackageManager, times(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals("test.package2", mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -804,12 +893,14 @@
         mClientManager.removeSubscriber(mSubscriberClient1);
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
     public void testRemoveSubscriber_NotRegistered() {
         mClientManager.removeSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
@@ -821,17 +912,17 @@
 
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
     public void testOnUserSwitch_RemoveSubscriber() {
         mClientManager.addSubscriber(mSubscriberClient1);
 
-        mForegroundUserId = USER_ID_U11;
-        mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
-
+        notifyUserSwitched(USER_ID_U11, false);
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertTrue(mClientManager.getAllSubscribers().isEmpty());
     }
 
@@ -839,15 +930,17 @@
     public void testOnUserSwitch_RemoveSubscriber_AddNewSubscriber() {
         mClientManager.addSubscriber(mSubscriberClient1);
 
-        mForegroundUserId = USER_ID_U11;
-        mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+        notifyUserSwitched(USER_ID_U11, false);
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
 
         mCallingAppUid = UserHandle.getUid(USER_ID_U11, 0);
         when(mPackageManager.getNameForUid(mCallingAppUid)).thenReturn(TEST_PACKAGE);
         mClientManager.addSubscriber(mSubscriberClient2);
 
+        assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
         assertFalse(mClientManager.getAllSubscribers().contains(mSubscriberClient1));
         assertTrue(mClientManager.getAllSubscribers().contains(mSubscriberClient2));
     }
@@ -861,8 +954,7 @@
 
         mClientManager.addSubscriber(mSubscriberClient2);
 
-        mForegroundUserId = USER_ID_U11;
-        mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+        notifyUserSwitched(USER_ID_U11, false);
 
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         verify(mBrokerService, never()).removeDeadSubscriber(mSubscriberClient2);
@@ -875,10 +967,10 @@
         IVmsPublisherClient publisherClient = createPublisherClient();
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
-        mForegroundUserId = USER_ID_U11;
-        mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+        notifyUserSwitched(USER_ID_U11, false);
 
         verify(mBrokerService, never()).removeDeadSubscriber(subscriberClient);
         assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
@@ -890,6 +982,7 @@
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
         verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
     }
@@ -902,6 +995,7 @@
 
         mClientManager.onHalConnected(publisherClient, subscriberClient);
         verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
     }
@@ -911,11 +1005,13 @@
         IVmsPublisherClient publisherClient = createPublisherClient();
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         mClientManager.onHalDisconnected();
         verify(mPublisherService).onClientDisconnected(eq(HAL_CLIENT_NAME));
         verify(mBrokerService).removeDeadSubscriber(eq(subscriberClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.DISCONNECTED);
         assertFalse(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(subscriberClient));
     }
@@ -928,7 +1024,7 @@
     }
 
     private void resetContext() {
-        reset(mContext);
+        reset(mContext, mSystemClientLog, mUserClientLog);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(true);
         when(mContext.getResources()).thenReturn(mResources);
@@ -939,30 +1035,27 @@
     }
 
     private void notifyUserSwitched(int foregroundUserId, boolean isForegroundUserUnlocked) {
-        notifyUserAction(foregroundUserId, isForegroundUserUnlocked, Intent.ACTION_USER_SWITCHED);
+        when(mUserManager.isUserUnlocked(foregroundUserId)).thenReturn(isForegroundUserUnlocked);
+        mForegroundUserId = foregroundUserId; // Member variable used by verifyUserBind()
+        mClientManager.mUserCallback.onSwitchUser(foregroundUserId);
     }
 
     private void notifyUserUnlocked(int foregroundUserId, boolean isForegroundUserUnlocked) {
-        notifyUserAction(foregroundUserId, isForegroundUserUnlocked, Intent.ACTION_USER_UNLOCKED);
-    }
-
-    // Sets the current foreground user + unlock state and dispatches the specified intent action
-    private void notifyUserAction(int foregroundUserId, boolean isForegroundUserUnlocked,
-            String action) {
-        mForegroundUserId = foregroundUserId; // Member variable used by verifyUserBind()
-        when(mUserManagerHelper.getCurrentForegroundUserId()).thenReturn(foregroundUserId);
-
-        reset(mUserManager);
         when(mUserManager.isUserUnlocked(foregroundUserId)).thenReturn(isForegroundUserUnlocked);
-
-        mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent(action));
+        mClientManager.mUserCallback.onUserLockChanged(foregroundUserId, isForegroundUserUnlocked);
     }
 
     private void verifySystemBind(int times) {
+        verify(mSystemClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
         verifyBind(times, SYSTEM_CLIENT_COMPONENT, UserHandle.SYSTEM);
     }
 
     private void verifyUserBind(int times) {
+        if (mForegroundUserId == USER_ID) {
+            verify(mUserClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
+        } else if (mForegroundUserId == USER_ID_U11) {
+            verify(mUserClientLog2, times(times)).logConnectionState(ConnectionState.CONNECTING);
+        }
         verifyBind(times, USER_CLIENT_COMPONENT, UserHandle.of(mForegroundUserId));
     }
 
@@ -975,6 +1068,11 @@
                 eq(Context.BIND_AUTO_CREATE), any(Handler.class), eq(user));
     }
 
+    private void verifyAndRunRebindTask() {
+        verify(mHandler).postDelayed(mRebindCaptor.capture(), eq(MILLIS_BEFORE_REBIND));
+        mRebindCaptor.getValue().run();
+    }
+
     private void verifyOnClientConnected(String publisherName, IBinder binder) {
         ArgumentCaptor<IVmsPublisherClient> clientCaptor =
                 ArgumentCaptor.forClass(IVmsPublisherClient.class);
diff --git a/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
new file mode 100755
index 0000000..8beb1f4
--- /dev/null
+++ b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+if [ -z "$ANDROID_PRODUCT_OUT" ]; then
+    echo "ANDROID_PRODUCT_OUT not set"
+    exit
+fi
+DISP_ID=1
+if [[ $# -eq 1 ]]; then
+  echo "$1"
+  DISP_ID=$1
+fi
+echo "Use display:$DISP_ID"
+
+adb root
+# Check always crashing one
+echo "Start AlwaysCrashingActivity in fixed mode"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.AlwaysCrashingActivity
+sleep 1
+read -p "AlwaysCrashingAvtivity should not be tried any more. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# suspend-resume
+echo "Check retry for suspend - resume"
+adb shell setprop android.car.garagemodeduration 1
+adb shell dumpsys car_service suspend
+adb shell dumpsys car_service resume
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# starting other Activity
+echo "Switch to no crash Activity"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.NoCrashActivity
+read -p "NoCrashAvtivity should have been shown. Press Enter"
+# stating other non-fixed Activity
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "NoCrashAvtivity should be shown after showing EmptyActivity. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "NoCrashActivity should be shown. Press Enter"
+# stop the mode
+echo "Stop fixed activity mode"
+adb shell dumpsys car_service stop-fixed-activity-mode $DISP_ID
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "EmptyActivity should be shown. Press Enter"
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 0745945..72332d9 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -238,10 +238,8 @@
         // If an override user is present and a real user, return it
         if (bootUserOverride != BOOT_USER_NOT_FOUND
                 && allUsers.contains(bootUserOverride)) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Boot user id override found for initial user, user id: "
-                        + bootUserOverride);
-            }
+            Log.i(TAG, "Boot user id override found for initial user, user id: "
+                    + bootUserOverride);
             return bootUserOverride;
         }
 
@@ -249,19 +247,15 @@
         int lastActiveUser = getLastActiveUser();
         if (lastActiveUser != UserHandle.USER_SYSTEM
                 && allUsers.contains(lastActiveUser)) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Last active user loaded for initial user, user id: "
-                        + lastActiveUser);
-            }
+            Log.i(TAG, "Last active user loaded for initial user, user id: "
+                    + lastActiveUser);
             return lastActiveUser;
         }
 
         // If all else fails, return the smallest user id
         int returnId = Collections.min(allUsers);
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Saved ids were invalid. Returning smallest user id, user id: "
-                    + returnId);
-        }
+        Log.i(TAG, "Saved ids were invalid. Returning smallest user id, user id: "
+                + returnId);
         return returnId;
     }