Clean-up duplicated product configuration am: 8ff906e642 am: de5ef6972f

Change-Id: I69cde8fc665b9227cf89609704cc31642cea9f33
diff --git a/FrameworkPackageStubs/AndroidManifest.xml b/FrameworkPackageStubs/AndroidManifest.xml
index e1ad903..c88ffdb 100644
--- a/FrameworkPackageStubs/AndroidManifest.xml
+++ b/FrameworkPackageStubs/AndroidManifest.xml
@@ -55,6 +55,15 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.dir/audio"/>
             </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
+                <data android:mimeType="video/*" />
+                <data android:mimeType="image/*" />
+            </intent-filter>
         </activity>
 
         <!-- Settings package stubs -->
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 67a00e3..aa2a607 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;
@@ -55,6 +58,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.TransactionTooLargeException;
 import android.os.UserHandle;
 import android.util.Log;
 
@@ -680,6 +684,8 @@
 
     private final Context mContext;
 
+    private final Exception mConstructionStack;
+
     private final Object mLock = new Object();
 
     @GuardedBy("mLock")
@@ -725,11 +731,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 +747,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 +775,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 +789,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 +831,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 +841,7 @@
      */
     @Nullable
     public static Car createCar(Context context, @Nullable Handler handler) {
+        assertNonNullContext(context);
         Car car = null;
         IBinder service = null;
         boolean started = false;
@@ -842,6 +855,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 +919,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 +938,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 +1019,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 +1054,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 +1170,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 +1192,7 @@
                     }
                     mServiceMap.put(serviceName, manager);
                 } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
+                    handleRemoteExceptionFromCarService(e);
                 }
             }
         }
@@ -1171,84 +1208,156 @@
         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) {
+        if (e instanceof TransactionTooLargeException) {
+            Log.w(TAG_CAR, "Car service threw TransactionTooLargeException", e);
+            throw new CarTransactionException(e, "Car service threw TransactionTooLargException");
+        } else {
+            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) {
+        if (e instanceof TransactionTooLargeException) {
+            Log.w(TAG_CAR, "Car service threw TransactionTooLargeException, client:"
+                    + service.getPackageName() + ","
+                    + service.getClass().getSimpleName(), e);
+            throw new CarTransactionException(e, "Car service threw TransactionTooLargeException, "
+                + "client: %s, %s", service.getPackageName(), service.getClass().getSimpleName());
+        } else {
+            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 +1390,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/CarTransactionException.java b/car-lib/src/android/car/CarTransactionException.java
new file mode 100644
index 0000000..c0e7e2f
--- /dev/null
+++ b/car-lib/src/android/car/CarTransactionException.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 android.car;
+
+/**
+ * Runtime exception thrown when a transaction-level binder exception has occurred.
+ *
+ * This allows clients to log or recover transaction errors as appropriate.
+ *
+ * @hide
+ */
+public class CarTransactionException extends UnsupportedOperationException {
+    CarTransactionException(Throwable cause, String msg, Object... args) {
+        super(String.format(msg, args), cause);
+    }
+}
+
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/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index ab7c906..d81b7ad 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -153,5 +153,14 @@
          */
         public static final String KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER =
                 "android.car.ENABLE_INITIAL_NOTICE_SCREEN_TO_USER";
+
+        /**
+         * Key to indicate Setup Wizard is in progress. It differs from USER_SETUP_COMPLETE in
+         * that this flag can be reset to 0 in deferred Setup Wizard flow.
+         * The value is boolean (1 or 0).
+         * @hide
+         */
+        public static final String KEY_SETUP_WIZARD_IN_PROGRESS =
+                "android.car.SETUP_WIZARD_IN_PROGRESS";
     }
 }
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-usb-handler/src/android/car/usb/handler/BootUsbService.java b/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
index 909be07..5d29f54 100644
--- a/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
+++ b/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
@@ -46,14 +46,14 @@
     static final String USB_DEVICE_LIST_KEY = "usb_device_list";
 
     private ArrayList<UsbDevice> mDeviceList;
-
-    private class UserSwitchBroadcastReceiver extends BroadcastReceiver {
+    private boolean mReceiverRegistered = false;
+    private final BroadcastReceiver mUserSwitchBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             processDevices();
-            unregisterReceiver(this);
+            unregisterUserSwitchReceiver();
         }
-    }
+    };
 
     @Override
     public Binder onBind(Intent intent) {
@@ -86,8 +86,7 @@
         // immediately.
         if (ActivityManager.getCurrentUser() == UserHandle.USER_SYSTEM) {
             Log.d(TAG, "Current user is still the system user, waiting for user switch");
-            registerReceiver(
-                    new UserSwitchBroadcastReceiver(), new IntentFilter(ACTION_USER_SWITCHED));
+            registerUserSwitchReceiver();
         } else {
             processDevices();
         }
@@ -95,6 +94,11 @@
         return START_NOT_STICKY;
     }
 
+    @Override
+    public void onDestroy() {
+        unregisterUserSwitchReceiver();
+    }
+
     private void processDevices() {
         Log.d(TAG, "Processing connected USB devices and starting handlers");
         for (UsbDevice device : mDeviceList) {
@@ -110,4 +114,18 @@
         manageDevice.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
         context.startActivityAsUser(manageDevice, UserHandle.CURRENT);
     }
+
+    private void registerUserSwitchReceiver() {
+        if (!mReceiverRegistered) {
+            registerReceiver(mUserSwitchBroadcastReceiver, new IntentFilter(ACTION_USER_SWITCHED));
+            mReceiverRegistered = true;
+        }
+    }
+
+    private void unregisterUserSwitchReceiver() {
+        if (mReceiverRegistered) {
+            unregisterReceiver(mUserSwitchBroadcastReceiver);
+            mReceiverRegistered = false;
+        }
+    }
 }
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/core/res/res/values/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
index 9217647..34fd92f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
@@ -18,4 +18,5 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Default name of the owner user [CHAR LIMIT=20] -->
     <string name="owner_name">Driver</string>
+    <string name="permlab_accessCoarseLocation">access approximate location only in the foreground</string>
 </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..05c7b3f 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -12,7 +12,8 @@
 # Allow Car Service to register/access itself with ServiceManager
 add_service(carservice_app, carservice_service)
 
-allow carservice_app wifi_service:service_manager find;
+# Allow Car Service to register its stats service with ServiceManager
+add_service(carservice_app, carstats_service)
 
 # Allow Car Service to access certain system services.
 # Keep alphabetically sorted.
@@ -41,6 +42,8 @@
     telecom_service
     uimode_service
     voiceinteraction_service
+    wifi_service
+    wifiscanner_service
 }:service_manager find;
 
 # Read and write /data/data subdirectory.
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/car_product/sepolicy/test/kitchensink_app.te b/car_product/sepolicy/test/kitchensink_app.te
index 0ab9c43..af00c45 100644
--- a/car_product/sepolicy/test/kitchensink_app.te
+++ b/car_product/sepolicy/test/kitchensink_app.te
@@ -10,6 +10,8 @@
     accessibility_service
     activity_service
     activity_task_service
+    audio_service
+    audioserver_service
     autofill_service
     carservice_service
     connectivity_service
@@ -20,6 +22,7 @@
     input_method_service
     input_service
     location_service
+    mediaserver_service
     network_management_service
     power_service
     sensorservice_service
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 4f6a82a..cfd45e5 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/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 2f37e61..d4daac2 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -297,7 +297,7 @@
         if (location == null) {
             logd("Not storing null location");
         } else {
-            logd("Storing location: " + location);
+            logd("Storing location");
             AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
             FileOutputStream fos = null;
             try {
@@ -437,7 +437,7 @@
                 }
             }
         }
-        logd("Injected location " + location + " with result " + success + " on attempt "
+        logd("Injected location with result " + success + " on attempt "
                 + attemptCount);
         if (success) {
             return;
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 4841b0d..f3c9b79 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,39 @@
         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 (mCurrentUser == 0) {
+            return;
+        }
+        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 +219,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 +345,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) {
@@ -342,9 +389,12 @@
 
     /**
      * Attempts to stop the current source using MediaController.TransportControls.stop()
+     * This method also unregisters callbacks to the active media controller before calling stop(),
+     * to preserve the PlaybackState before stopping.
      */
-    private void stop() {
+    private void stopAndUnregisterCallback() {
         if (mActiveUserMediaController != null) {
+            mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
             if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
                 Log.d(CarLog.TAG_MEDIA, "stopping " + mActiveUserMediaController.getPackageName());
             }
@@ -472,25 +522,26 @@
             return;
         }
 
-        stop();
+        stopAndUnregisterCallback();
 
-        mStartPlayback = mPlayOnMediaSourceChanged;
+        mActiveUserMediaController = null;
         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();
+
+        startMediaConnectorService(shouldStartPlayback(mPlayOnMediaSourceChangedConfig),
+                new UserHandle(mCurrentUser));
     }
 
     private void notifyListeners() {
@@ -509,9 +560,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 +615,6 @@
         return false;
     }
 
-
     private boolean isMediaService(@NonNull ComponentName componentName) {
         return getMediaService(componentName) != null;
     }
@@ -612,6 +662,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 +680,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 +714,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 +745,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..a1f067b 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,40 @@
     // 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 mShutdownOnNextSuspend;
+    @GuardedBy("mLock")
     private boolean mIsBooting = true;
+    @GuardedBy("mLock")
+    private boolean mIsResuming;
 
     private final CarUserManagerHelper mCarUserManagerHelper;
 
@@ -160,7 +172,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 +192,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 +207,6 @@
         }
         mSystemInterface.stopDisplayStateMonitoring();
         mPowerManagerListeners.kill();
-        mListenersWeAreWaitingFor.clear();
         mSystemInterface.releaseAllWakeLocks();
     }
 
@@ -205,6 +217,7 @@
         writer.print(",mProcessingStartTime:" + mProcessingStartTime);
         writer.print(",mLastSleepEntryTime:" + mLastSleepEntryTime);
         writer.print(",mNextWakeupSec:" + mNextWakeupSec);
+        writer.print(",mShutdownOnNextSuspend:" + mShutdownOnNextSuspend);
         writer.print(",mShutdownOnFinish:" + mShutdownOnFinish);
         writer.println(",sShutdownPrepareTimeMs:" + sShutdownPrepareTimeMs);
     }
@@ -212,7 +225,7 @@
     @Override
     public void onApPowerStateChange(PowerState state) {
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             mPendingPowerStates.addFirst(new CpmsState(state));
             handler = mHandler;
         }
@@ -220,8 +233,11 @@
     }
 
     @VisibleForTesting
-    protected void clearIsBooting() {
-        mIsBooting = false;
+    protected void clearIsBootingOrResuming() {
+        synchronized (mLock) {
+            mIsBooting = false;
+            mIsResuming = false;
+        }
     }
 
     /**
@@ -230,7 +246,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 +256,7 @@
     private void doHandlePowerStateChange() {
         CpmsState state;
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             state = mPendingPowerStates.peekFirst();
             mPendingPowerStates.clear();
             if (state == null) {
@@ -295,6 +311,7 @@
                 mHal.sendWaitForVhal();
                 break;
             case CarPowerStateListener.SHUTDOWN_CANCELLED:
+                mShutdownOnNextSuspend = false; // This cancels the "NextSuspend"
                 mHal.sendShutdownCancel();
                 break;
             case CarPowerStateListener.SUSPEND_EXIT:
@@ -304,10 +321,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 +360,12 @@
     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 = mShutdownOnNextSuspend
+                    || !mHal.isDeepSleepAllowed()
+                    || !mSystemInterface.isSystemSupportingDeepSleep()
+                    || !newState.mCanSleep;
+        }
         if (newState.mCanPostpone) {
             Log.i(CarLog.TAG_POWER, "starting shutdown prepare");
             sendPowerManagerEvent(CarPowerStateListener.SHUTDOWN_PREPARE);
@@ -333,7 +373,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 +395,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) {
@@ -378,17 +424,16 @@
         } else {
             doHandleDeepSleep(simulatedMode);
         }
+        mShutdownOnNextSuspend = false;
     }
 
-    @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 +452,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 +478,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) {
@@ -464,7 +509,7 @@
                 listener.onStateChanged(newState);
             } catch (RemoteException e) {
                 // It's likely the connection snapped. Let binder death handle the situation.
-                Log.e(CarLog.TAG_POWER, "onStateChanged() call failed: " + e, e);
+                Log.e(CarLog.TAG_POWER, "onStateChanged() call failed", e);
             }
         }
         listenerList.finishBroadcast();
@@ -475,28 +520,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 +587,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 +620,7 @@
 
     public void handleMainDisplayChanged(boolean on) {
         PowerHandler handler;
-        synchronized (CarPowerManagementService.this) {
+        synchronized (mLock) {
             handler = mHandler;
         }
         handler.handleMainDisplayStateChange(on);
@@ -586,8 +634,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 +681,9 @@
     @Override
     public void requestShutdownOnNextSuspend() {
         ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_POWER);
-        mShutdownOnFinish = true;
+        synchronized (mLock) {
+            mShutdownOnNextSuspend = true;
+        }
     }
 
     @Override
@@ -639,27 +694,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 +732,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 +824,7 @@
 
         @Override
         public void run() {
-            synchronized (CarPowerManagementService.this) {
+            synchronized (mLock) {
                 if (!mTimerActive) {
                     // Ignore timer expiration since we got cancelled
                     return;
@@ -927,9 +986,9 @@
         }
         handler.handlePowerStateChange();
 
-        synchronized (mSimulationSleepObject) {
+        synchronized (mSimulationWaitObject) {
             mWakeFromSimulatedSleep = true;
-            mSimulationSleepObject.notify();
+            mSimulationWaitObject.notify();
         }
     }
 
@@ -940,12 +999,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 +1015,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..0c50da9 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,9 @@
                 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
+            // allowing for nested flag selection
+            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 +505,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 +554,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 +600,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 +731,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/VmsLayersAvailability.java b/service/src/com/android/car/VmsLayersAvailability.java
index 2e17a89..a9bbc7a 100644
--- a/service/src/com/android/car/VmsLayersAvailability.java
+++ b/service/src/com/android/car/VmsLayersAvailability.java
@@ -109,9 +109,6 @@
             mPotentialLayersAndPublishers.clear();
             mAvailableAssociatedLayers = Collections.EMPTY_SET;
             mUnavailableAssociatedLayers = Collections.EMPTY_SET;
-            if (mSeq + 1 < mSeq) {
-                throw new IllegalStateException("Sequence is about to loop");
-            }
             mSeq += 1;
         }
     }
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 3029a9e..def10dd 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.getVmsClientLogger(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.getVmsClientLogger(-1)
+                        .logPacketDropped(layer, payloadLength);
             }
 
             for (IVmsSubscriberClient listener : listeners) {
+                int subscriberUid = mClientManager.getSubscriberUid(listener);
                 try {
                     listener.onVmsMessageReceived(layer, payload);
+                    mStatsService.getVmsClientLogger(subscriberUid)
+                            .logPacketReceived(layer, payloadLength);
                 } catch (RemoteException ex) {
+                    mStatsService.getVmsClientLogger(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/audio/CarAudioFocus.java b/service/src/com/android/car/audio/CarAudioFocus.java
index 165caad..286a045 100644
--- a/service/src/com/android/car/audio/CarAudioFocus.java
+++ b/service/src/com/android/car/audio/CarAudioFocus.java
@@ -113,6 +113,10 @@
                             mAfi.getPackageName())
                     == PackageManager.PERMISSION_GRANTED);
         }
+
+        String getUsageName() {
+            return mAfi.getAttributes().usageToString();
+        }
     }
 
 
@@ -171,8 +175,9 @@
     // The default audio framework's behavior is to remove the previous entry in the stack (no-op
     // if the requester is already holding focus).
     int evaluateFocusRequest(AudioFocusInfo afi) {
-        Log.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest()) + " request for client "
-                + afi.getClientId());
+        Log.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest())
+                + " request for client " + afi.getClientId()
+                + " with usage " + afi.getAttributes().usageToString());
 
         // Is this a request for premanant focus?
         // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
@@ -588,14 +593,17 @@
     public synchronized void dump(String indent, PrintWriter writer) {
         writer.printf("%s*CarAudioFocus*\n", indent);
 
-        writer.printf("%s\tCurrent Focus Holders:\n", indent);
+        String innerIndent = indent + "\t";
+        writer.printf("%sCurrent Focus Holders:\n", innerIndent);
         for (String clientId : mFocusHolders.keySet()) {
-            writer.printf("%s\t\t%s\n", indent, clientId);
+            writer.printf("%s\t%s - %s\n", innerIndent, clientId,
+                    mFocusHolders.get(clientId).getUsageName());
         }
 
-        writer.printf("%s\tTransient Focus Losers:\n", indent);
+        writer.printf("%sTransient Focus Losers:\n", innerIndent);
         for (String clientId : mFocusLosers.keySet()) {
-            writer.printf("%s\t\t%s\n", indent, clientId);
+            writer.printf("%s\t%s - %s\n", innerIndent, clientId,
+                    mFocusLosers.get(clientId).getUsageName());
         }
     }
 
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/pm/CarPackageManagerService.java b/service/src/com/android/car/pm/CarPackageManagerService.java
index 01d5500..54d577a 100644
--- a/service/src/com/android/car/pm/CarPackageManagerService.java
+++ b/service/src/com/android/car/pm/CarPackageManagerService.java
@@ -213,7 +213,7 @@
         if (DBG_POLICY_SET) {
             Log.i(CarLog.TAG_PACKAGE, "policy setting from binder call, client:" + packageName);
         }
-        doSetAppBlockingPolicy(packageName, policy, flags, true /*setNow*/);
+        doSetAppBlockingPolicy(packageName, policy, flags);
     }
 
     /**
@@ -224,8 +224,8 @@
         mSystemActivityMonitoringService.restartTask(taskId);
     }
 
-    private void doSetAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy, int flags,
-            boolean setNow) {
+    private void doSetAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy,
+            int flags) {
         if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CONTROL_APP_BLOCKING)
                 != PackageManager.PERMISSION_GRANTED) {
             throw new SecurityException(
@@ -240,15 +240,22 @@
             throw new IllegalArgumentException(
                     "Cannot set both FLAG_SET_POLICY_ADD and FLAG_SET_POLICY_REMOVE flag");
         }
-        mHandler.requestUpdatingPolicy(packageName, policy, flags);
-        if (setNow) {
-            mHandler.requestPolicySetting();
+        synchronized (this) {
             if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
-                synchronized (policy) {
-                    try {
-                        policy.wait();
-                    } catch (InterruptedException e) {
+                mWaitingPolicies.add(policy);
+            }
+        }
+        mHandler.requestUpdatingPolicy(packageName, policy, flags);
+        if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
+            synchronized (this) {
+                try {
+                    while (mWaitingPolicies.contains(policy)) {
+                        wait();
                     }
+                } catch (InterruptedException e) {
+                    // Pass it over binder call
+                    throw new IllegalStateException(
+                            "Interrupted while waiting for policy completion", e);
                 }
             }
         }
@@ -394,7 +401,8 @@
                 }
                 mProxies.clear();
             }
-            wakeupClientsWaitingForPolicySettingLocked();
+            mWaitingPolicies.clear();
+            notifyAll();
         }
         mContext.unregisterReceiver(mPackageParsingEventReceiver);
         mContext.unregisterReceiver(mUserSwitchedEventReceiver);
@@ -456,23 +464,6 @@
         notifyAll();
     }
 
-    @GuardedBy("this")
-    private void wakeupClientsWaitingForPolicySettingLocked() {
-        for (CarAppBlockingPolicy waitingPolicy : mWaitingPolicies) {
-            synchronized (waitingPolicy) {
-                waitingPolicy.notifyAll();
-            }
-        }
-        mWaitingPolicies.clear();
-    }
-
-    private void doSetPolicy() {
-        synchronized (this) {
-            wakeupClientsWaitingForPolicySettingLocked();
-        }
-        blockTopActivitiesIfNecessary();
-    }
-
     private void doUpdatePolicy(String packageName, CarAppBlockingPolicy policy, int flags) {
         if (DBG_POLICY_SET) {
             Log.i(CarLog.TAG_PACKAGE, "setting policy from:" + packageName + ",policy:" + policy +
@@ -497,7 +488,8 @@
                 clientPolicy.replaceWhitelists(whitelistWrapper);
             }
             if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
-                mWaitingPolicies.add(policy);
+                mWaitingPolicies.remove(policy);
+                notifyAll();
             }
             if (DBG_POLICY_SET) {
                 Log.i(CarLog.TAG_PACKAGE, "policy set:" + dumpPoliciesLocked(false));
@@ -855,7 +847,6 @@
         policyIntent.setAction(CarAppBlockingPolicyService.SERVICE_INTERFACE);
         List<ResolveInfo> policyInfos = mPackageManager.queryIntentServices(policyIntent, 0);
         if (policyInfos == null) { //no need to wait for service binding and retrieval.
-            mHandler.requestPolicySetting();
             return;
         }
         LinkedList<AppBlockingPolicyProxy> proxies = new LinkedList<>();
@@ -892,7 +883,6 @@
 
     private void doHandlePolicyConnection(AppBlockingPolicyProxy proxy,
             CarAppBlockingPolicy policy) {
-        boolean shouldSetPolicy = false;
         synchronized (this) {
             if (mProxies == null) {
                 proxy.disconnect();
@@ -900,7 +890,6 @@
             }
             mProxies.remove(proxy);
             if (mProxies.size() == 0) {
-                shouldSetPolicy = true;
                 mProxies = null;
             }
         }
@@ -910,13 +899,10 @@
                     Log.i(CarLog.TAG_PACKAGE, "policy setting from policy service:" +
                             proxy.getPackageName());
                 }
-                doSetAppBlockingPolicy(proxy.getPackageName(), policy, 0, false /*setNow*/);
+                doSetAppBlockingPolicy(proxy.getPackageName(), policy, 0);
             }
         } finally {
             proxy.disconnect();
-            if (shouldSetPolicy) {
-                mHandler.requestPolicySetting();
-            }
         }
     }
 
@@ -1186,11 +1172,10 @@
      * Reading policy and setting policy can take time. Run it in a separate handler thread.
      */
     private class PackageHandler extends Handler {
-        private final int MSG_INIT = 0;
-        private final int MSG_PARSE_PKG = 1;
-        private final int MSG_SET_POLICY = 2;
-        private final int MSG_UPDATE_POLICY = 3;
-        private final int MSG_RELEASE = 4;
+        private static final int MSG_INIT = 0;
+        private static final int MSG_PARSE_PKG = 1;
+        private static final int MSG_UPDATE_POLICY = 2;
+        private static final int MSG_RELEASE = 3;
 
         private PackageHandler(Looper looper) {
             super(looper);
@@ -1203,17 +1188,11 @@
 
         private void requestRelease() {
             removeMessages(MSG_INIT);
-            removeMessages(MSG_SET_POLICY);
             removeMessages(MSG_UPDATE_POLICY);
             Message msg = obtainMessage(MSG_RELEASE);
             sendMessage(msg);
         }
 
-        private void requestPolicySetting() {
-            Message msg = obtainMessage(MSG_SET_POLICY);
-            sendMessage(msg);
-        }
-
         private void requestUpdatingPolicy(String packageName, CarAppBlockingPolicy policy,
                 int flags) {
             Pair<String, CarAppBlockingPolicy> pair = new Pair<>(packageName, policy);
@@ -1242,9 +1221,6 @@
                 case MSG_PARSE_PKG:
                     doParseInstalledPackages();
                     break;
-                case MSG_SET_POLICY:
-                    doSetPolicy();
-                    break;
                 case MSG_UPDATE_POLICY:
                     Pair<String, CarAppBlockingPolicy> pair =
                             (Pair<String, CarAppBlockingPolicy>) msg.obj;
diff --git a/service/src/com/android/car/pm/VendorServiceController.java b/service/src/com/android/car/pm/VendorServiceController.java
index 189370e..b20dc89 100644
--- a/service/src/com/android/car/pm/VendorServiceController.java
+++ b/service/src/com/android/car/pm/VendorServiceController.java
@@ -251,7 +251,7 @@
      * Represents connection to the vendor service.
      */
     private static class VendorServiceConnection implements ServiceConnection {
-        private static final int REBIND_DELAY_MS = 1000;
+        private static final int REBIND_DELAY_MS = 5000;
         private static final int MAX_RECENT_FAILURES = 5;
         private static final int FAILURE_COUNTER_RESET_TIMEOUT = 5 * 60 * 1000; // 5 min.
         private static final int MSG_REBIND = 0;
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..d74fb8c
--- /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.VmsClientLogger.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<VmsClientLogger, 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, VmsClientLogger> mVmsClientStats = new ArrayMap<>();
+
+    public CarStatsService(Context context) {
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+    }
+
+    /**
+     * Gets a logger for the VMS client with a given UID.
+     */
+    public VmsClientLogger getVmsClientLogger(int clientUid) {
+        synchronized (mVmsClientStats) {
+            return mVmsClientStats.computeIfAbsent(
+                    clientUid,
+                    uid -> {
+                        String packageName = mPackageManager.getNameForUid(uid);
+                        if (DEBUG) {
+                            Log.d(TAG, "Created VmsClientLog: " + packageName);
+                        }
+                        return new VmsClientLogger(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(VmsClientLogger::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/VmsClientLogger.java b/service/src/com/android/car/stats/VmsClientLogger.java
new file mode 100644
index 0000000..948db05
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientLogger.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 VmsClientLogger {
+    /**
+     * 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<>();
+
+    VmsClientLogger(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..9fbe1dd
--- /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) {
+        mUid = uid;
+
+        mLayerType = layer.getType();
+        mLayerChannel = layer.getSubtype();
+        mLayerVersion = layer.getVersion();
+    }
+
+    /**
+     * Copy constructor for entries exported from {@link VmsClientLogger}.
+     */
+    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..f2c4813 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.VmsClientLogger;
+import com.android.car.stats.VmsClientLogger.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.getVmsClientLogger(Process.myUid())
+                .logConnectionState(ConnectionState.CONNECTED);
     }
 
     /**
@@ -325,14 +322,13 @@
         synchronized (mLock) {
             if (mHalClient != null) {
                 mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
+                mStatsService.getVmsClientLogger(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.isUserUnlockingOrUnlocked(mCurrentUser)) {
+                Log.i(TAG, "Waiting for foreground user " + mCurrentUser + " 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;
         }
 
+        VmsClientLogger statsLog = mStatsService.getVmsClientLogger(
+                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 VmsClientLogger 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, VmsClientLogger 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/Android.mk b/tests/BugReportApp/Android.mk
index f948b82..186fe4b 100644
--- a/tests/BugReportApp/Android.mk
+++ b/tests/BugReportApp/Android.mk
@@ -37,7 +37,8 @@
 LOCAL_DEX_PREOPT := false
 
 LOCAL_JAVA_LIBRARIES += \
-    android.car
+    android.car \
+    br_google_auto_value_target
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     androidx.recyclerview_recyclerview \
@@ -56,6 +57,11 @@
 
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.google.android.car.bugreport
 
+# Explicitly define annotation processors even if javac can find them from
+# LOCAL_STATIC_JAVA_LIBRARIES.
+LOCAL_ANNOTATION_PROCESSORS := br_google_auto_value
+LOCAL_ANNOTATION_PROCESSOR_CLASSES := com.google.auto.value.processor.AutoValueProcessor
+
 include $(BUILD_PACKAGE)
 
 # ====  prebuilt library  ========================
@@ -76,3 +82,28 @@
     br_apache_commons:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar
 
 include $(BUILD_MULTI_PREBUILT)
+
+# Following shenanigans are needed for LOCAL_ANNOTATION_PROCESSORS.
+
+# ====  prebuilt host libraries  ========================
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+    br_google_auto_value:../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+
+include $(BUILD_HOST_PREBUILT)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_MODULE := br_google_auto_value_target
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := ../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+LOCAL_UNINSTALLABLE_MODULE := true
+
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/BugReportApp/AndroidManifest.xml b/tests/BugReportApp/AndroidManifest.xml
index 90958c7..d0b4cf3 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="11"
+          android:versionName="1.7.0">
 
     <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
@@ -29,8 +29,13 @@
     <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"/>
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
-    <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"
@@ -42,15 +47,26 @@
             </intent-filter>
         </activity>
 
-        <!-- singleInstance allows starting bugreport dialog when BugReportInfoActivity is open. -->
+        <!--
+          singleInstance allows starting bugreport dialog when BugReportInfoActivity is open.
+        -->
         <activity android:name=".BugReportActivity"
                   android:theme="@android:style/Theme.DeviceDefault.Dialog"
                   android:exported="true"
-                  android:launchMode="singleInstance">
+                  android:launchMode="singleInstance"
+                  android:excludeFromRecents="true">
+            <meta-data android:name="distractionOptimized" android:value="true"/>
+            <intent-filter>
+                <action android:name="com.google.android.car.bugreport.action.START_SILENT"/>
+            </intent-filter>
         </activity>
 
         <service android:name=".BugReportService"
-                 android:exported="false"/>
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="com.google.android.car.bugreport.action.START_SILENT"/>
+            </intent-filter>
+        </service>
 
         <service android:name="com.google.android.car.bugreport.UploadJob"
                  android:permission="android.permission.BIND_JOB_SERVICE"
diff --git a/tests/BugReportApp/README.md b/tests/BugReportApp/README.md
index 2dac8e9..7ed9375 100644
--- a/tests/BugReportApp/README.md
+++ b/tests/BugReportApp/README.md
@@ -38,16 +38,31 @@
 [overlayed](https://source.android.com/setup/develop/new-device#use-resource-overlays)
 for specific products.
 
+### Config
+
+Configs are defined in `Config.java`.
+
 ### System Properties
 
-- `android.car.bugreport.disableautoupload` - set it to `true` to disable auto-upload to Google
-   Cloud, and allow users to manually upload or copy the bugreports to flash drive.
+- `android.car.bugreport.enableautoupload` - please see Config#ENABLE_AUTO_UPLOAD to learn more.
+- `android.car.bugreport.force_enable` - set it `true` to enable bugreport app on **all builds**.
 
 ### Upload configuration
 
 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/.BugReportActivity`
+    - generates `MetaBugReport.Type.INTERACTIVE` bug report, shows audio message dialog before
+    collecting bugreport.
+2. `adb shell am start-foreground-service -a com.google.android.car.bugreport.action.START_SILENT com.google.android.car.bugreport/.BugReportService`
+    - generates `MetaBugReport.Type.SILENT` bug report, without audio message. It shows audio dialog
+    after collecting bugreport.
+
 ## 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..199b368 100644
--- a/tests/BugReportApp/res/layout/bug_info_view.xml
+++ b/tests/BugReportApp/res/layout/bug_info_view.xml
@@ -26,99 +26,68 @@
         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_add_audio_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_add_audio_button_text" />
+            <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/layout/bug_report_activity.xml b/tests/BugReportApp/res/layout/bug_report_activity.xml
index d5dce22..888c628 100644
--- a/tests/BugReportApp/res/layout/bug_report_activity.xml
+++ b/tests/BugReportApp/res/layout/bug_report_activity.xml
@@ -16,15 +16,16 @@
 -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@color/bugreport_background"
+    android:padding="@dimen/bug_report_padding"
+    android:orientation="vertical">
 
     <LinearLayout
         android:id="@+id/submit_bug_report_layout"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:padding="@dimen/bug_report_padding"
-        android:background="@color/bugreport_background"
         android:visibility="gone"
         android:orientation="vertical">
         <TextView
@@ -34,6 +35,16 @@
             android:textColor="@color/bugreport_text"
             android:gravity="center"
             android:text="@string/bugreport_dialog_title"/>
+        <TextView
+            android:id="@+id/bug_report_add_audio_to_existing"
+            android:layout_marginTop="@dimen/bug_report_voice_recording_margin_top"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="@color/bugreport_text"
+            android:gravity="center"
+            android:visibility="gone"
+            android:text="@string/bugreport_dialog_add_audio_to_existing"/>
         <com.google.android.car.bugreport.VoiceRecordingView
             android:id="@+id/voice_recording_view"
             android:layout_marginTop="@dimen/bug_report_voice_recording_margin_top"
@@ -65,23 +76,12 @@
             android:layout_marginTop="@dimen/bug_report_button_margin_top"
             android:padding="@dimen/bug_report_secondary_button_padding"
             android:text="@string/bugreport_dialog_cancel"/>
-        <Button
-            android:id="@+id/button_show_bugreports"
-            style="@style/standard_button"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/bug_report_button_margin_top"
-            android:padding="@dimen/bug_report_small_button_padding"
-            android:visibility="gone"
-            android:text="@string/bugreport_dialog_show_bugreports"/>
     </LinearLayout>
 
     <LinearLayout
         android:id="@+id/in_progress_layout"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="@color/bugreport_background"
-        android:padding="@dimen/bug_report_padding"
         android:visibility="gone"
         android:orientation="vertical">
         <TextView
@@ -115,4 +115,14 @@
             android:padding="@dimen/bug_report_secondary_button_padding"
             android:text="@string/bugreport_dialog_close"/>
     </LinearLayout>
+
+    <Button
+        android:id="@+id/button_show_bugreports"
+        style="@style/standard_button"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/bug_report_button_margin_top"
+        android:padding="@dimen/bug_report_small_button_padding"
+        android:visibility="gone"
+        android:text="@string/bugreport_dialog_show_bugreports"/>
 </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..b3b125a 100644
--- a/tests/BugReportApp/res/values/strings.xml
+++ b/tests/BugReportApp/res/values/strings.xml
@@ -15,35 +15,43 @@
      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>
+    <!-- A button: uploads bugreport with recorded audio message. -->
+    <string name="bugreport_dialog_upload">Upload</string>
+    <!-- A button: saves recorded audio message. -->
+    <string name="bugreport_dialog_save">Save</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>
+    <!-- %s is the timestamp of a bugreport. -->
+    <string name="bugreport_dialog_add_audio_to_existing">Audio message for bug report at %s</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>
+    <!-- A button to add audio message to the bugreport. It will show Save button on the dialog. -->
+    <string name="bugreport_add_audio_button_text">Add Audio</string>
+    <!-- A button to add audio message to the bugreport; it will show Upload button on the dialog. -->
+    <string name="bugreport_add_audio_upload_button_text">Add Audio &amp; Upload</string>
+    <string name="bugreport_move_button_text">Move to USB</string>
+    <string name="bugreport_upload_button_text">Upload</string>
+    <string name="bugreport_upload_gcs_button_text">Upload to GCS</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_bug_report_started">Bug reporting is started</string>
+    <string name="toast_status_failed">Bug report failed</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_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..f63d937 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,77 @@
 
 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;
+    static final int BUTTON_TYPE_ADD_AUDIO = 2;
 
     /** 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}, {@link #BUTTON_TYPE_MOVE} or
+         *                   {@link #BUTTON_TYPE_ADD_AUDIO}.
+         * @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;
+
+        /** Add Audio Button */
+        Button mAddAudioButton;
 
         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);
+            mAddAudioButton = itemView.findViewById(R.id.bug_info_add_audio_button);
         }
     }
 
-    private final List<MetaBugReport> mDataset;
+    private List<MetaBugReport> mDataset;
     private final ItemClickedListener mItemClickedListener;
+    private final Config mConfig;
 
-    BugInfoAdapter(List<MetaBugReport> dataSet, ItemClickedListener itemClickedListener) {
-        mDataset = dataSet;
+    BugInfoAdapter(ItemClickedListener itemClickedListener, Config config) {
         mItemClickedListener = itemClickedListener;
+        mDataset = new ArrayList<>();
+        mConfig = config;
+        // Allow RecyclerView to efficiently update UI; getItemId() is implemented below.
+        setHasStableIds(true);
     }
 
     @Override
@@ -93,22 +107,74 @@
     @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));
+        } else {
+            holder.mMoveButton.setEnabled(false);
+            holder.mMoveButton.setVisibility(View.GONE);
+        }
+        // Always enable upload to GCS button, because the app is enabled only for userdebug,
+        // and sometimes Config might not be properly set.
+        if (enableUserActionButtons) {
+            holder.mUploadButton.setText(R.string.bugreport_upload_gcs_button_text);
+            holder.mUploadButton.setEnabled(true);
+            holder.mUploadButton.setVisibility(View.VISIBLE);
+            holder.mUploadButton.setOnClickListener(
+                    view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport,
+                            holder));
+        } else {
+            holder.mUploadButton.setVisibility(View.GONE);
+            holder.mUploadButton.setEnabled(false);
+        }
+        if (bugreport.getStatus() == Status.STATUS_AUDIO_PENDING.getValue()) {
+            if (mConfig.getAutoUpload()) {
+                holder.mAddAudioButton.setText(R.string.bugreport_add_audio_upload_button_text);
+            } else {
+                holder.mAddAudioButton.setText(R.string.bugreport_add_audio_button_text);
+            }
+            holder.mAddAudioButton.setEnabled(true);
+            holder.mAddAudioButton.setVisibility(View.VISIBLE);
+            holder.mAddAudioButton.setOnClickListener(view ->
+                    mItemClickedListener.onItemClicked(BUTTON_TYPE_ADD_AUDIO, bugreport, holder));
+        } else {
+            holder.mAddAudioButton.setEnabled(false);
+            holder.mAddAudioButton.setVisibility(View.GONE);
+        }
+    }
+
+    /** 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..a66ce0f 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
@@ -15,7 +15,6 @@
  */
 package com.google.android.car.bugreport;
 
-import static com.google.android.car.bugreport.BugReportService.EXTRA_META_BUG_REPORT;
 import static com.google.android.car.bugreport.BugReportService.MAX_PROGRESS_VALUE;
 
 import android.Manifest;
@@ -25,6 +24,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;
@@ -41,14 +41,19 @@
 import android.util.Log;
 import android.view.View;
 import android.view.Window;
+import android.widget.Button;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Random;
@@ -65,33 +70,52 @@
 public class BugReportActivity extends Activity {
     private static final String TAG = BugReportActivity.class.getSimpleName();
 
+    /** Starts silent (no audio message recording) bugreporting. */
+    private static final String ACTION_START_SILENT =
+            "com.google.android.car.bugreport.action.START_SILENT";
+
+    /** This is deprecated action. Please start SILENT bugreport using {@link BugReportService}. */
+    private static final String ACTION_ADD_AUDIO =
+            "com.google.android.car.bugreport.action.ADD_AUDIO";
+
     private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000;
     private static final int AUDIO_PERMISSIONS_REQUEST_ID = 1;
 
-    private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
-            new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+    private static final String EXTRA_BUGREPORT_ID = "bugreport-id";
 
+    /**
+     * NOTE: mRecorder related messages are cleared when the activity finishes.
+     */
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
+    /** Look up string length, e.g. [ABCDEF]. */
+    static final int LOOKUP_STRING_LENGTH = 6;
+
     private TextView mInProgressTitleText;
     private ProgressBar mProgressBar;
     private TextView mProgressText;
+    private TextView mAddAudioText;
     private VoiceRecordingView mVoiceRecordingView;
     private View mVoiceRecordingFinishedView;
     private View mSubmitBugReportLayout;
     private View mInProgressLayout;
     private View mShowBugReportsButton;
+    private Button mSubmitButton;
 
     private boolean mBound;
     private boolean mAudioRecordingStarted;
-    private boolean mBugReportServiceStarted;
+    private boolean mIsNewBugReport;
+    private boolean mIsOnActivityStartedWithBugReportServiceBoundCalled;
+    private boolean mIsSubmitButtonClicked;
     private BugReportService mService;
     private MediaRecorder mRecorder;
     private MetaBugReport mMetaBugReport;
+    private File mAudioFile;
     private Car mCar;
     private CarDrivingStateManager mDrivingStateManager;
     private AudioManager mAudioManager;
     private AudioFocusRequest mLastAudioFocusRequest;
+    private Config mConfig;
 
     /** Defines callbacks for service binding, passed to bindService() */
     private ServiceConnection mConnection = new ServiceConnection() {
@@ -100,7 +124,7 @@
             BugReportService.ServiceBinder binder = (BugReportService.ServiceBinder) service;
             mService = binder.getService();
             mBound = true;
-            startAudioMessageRecording();
+            onActivityStartedWithBugReportServiceBound();
         }
 
         @Override
@@ -110,7 +134,7 @@
         }
     };
 
-    private final ServiceConnection mServiceConnection = new ServiceConnection() {
+    private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             try {
@@ -118,6 +142,8 @@
                         Car.CAR_DRIVING_STATE_SERVICE);
                 mDrivingStateManager.registerListener(
                         BugReportActivity.this::onCarDrivingStateChanged);
+                // Call onCarDrivingStateChanged(), because it's not called when Car is connected.
+                onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState());
             } catch (CarNotConnectedException e) {
                 Log.w(TAG, "Failed to get CarDrivingStateManager.", e);
             }
@@ -128,30 +154,24 @@
         }
     };
 
+    /**
+     * Builds an intent that starts {@link BugReportActivity} to add audio message to the existing
+     * bug report.
+     */
+    static Intent buildAddAudioIntent(Context context, MetaBugReport bug) {
+        Intent addAudioIntent = new Intent(context, BugReportActivity.class);
+        addAudioIntent.setAction(ACTION_ADD_AUDIO);
+        addAudioIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        addAudioIntent.putExtra(EXTRA_BUGREPORT_ID, bug.getId());
+        return addAudioIntent;
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
         super.onCreate(savedInstanceState);
-
         requestWindowFeature(Window.FEATURE_NO_TITLE);
-        setContentView(R.layout.bug_report_activity);
-
-        mInProgressTitleText = findViewById(R.id.in_progress_title_text);
-        mProgressBar = findViewById(R.id.progress_bar);
-        mProgressText = findViewById(R.id.progress_text);
-        mVoiceRecordingView = findViewById(R.id.voice_recording_view);
-        mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view);
-        mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout);
-        mInProgressLayout = findViewById(R.id.in_progress_layout);
-        mShowBugReportsButton = findViewById(R.id.button_show_bugreports);
-
-        mShowBugReportsButton.setOnClickListener(this::buttonShowBugReportsClick);
-        findViewById(R.id.button_submit).setOnClickListener(this::buttonSubmitClick);
-        findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick);
-        findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick);
-
-        mCar = Car.createCar(this, mServiceConnection);
-        mCar.connect();
-        mAudioManager = getSystemService(AudioManager.class);
 
         // Bind to BugReportService.
         Intent intent = new Intent(this, BugReportService.class);
@@ -163,26 +183,35 @@
         super.onStart();
 
         if (mBound) {
-            startAudioMessageRecording();
+            onActivityStartedWithBugReportServiceBound();
         }
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        if (!mBugReportServiceStarted && mAudioRecordingStarted) {
+        // If SUBMIT button is clicked, cancelling audio has been taken care of.
+        if (!mIsSubmitButtonClicked) {
             cancelAudioMessageRecording();
         }
         if (mBound) {
             mService.removeBugReportProgressListener();
         }
+        // Reset variables for the next onStart().
+        mAudioRecordingStarted = false;
+        mIsSubmitButtonClicked = false;
+        mIsOnActivityStartedWithBugReportServiceBoundCalled = false;
+        mMetaBugReport = null;
+        mAudioFile = null;
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
 
-        mHandler.removeCallbacksAndMessages(null);
+        if (mRecorder != null) {
+            mHandler.removeCallbacksAndMessages(/* token= */ mRecorder);
+        }
         if (mBound) {
             unbindService(mConnection);
             mBound = false;
@@ -194,7 +223,14 @@
     }
 
     private void onCarDrivingStateChanged(CarDrivingStateEvent event) {
-        if (event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED) {
+        // When adding audio message to the existing bugreport, do not show "Show Bug Reports"
+        // button, users either should explicitly Submit or Cancel.
+        if (mAudioRecordingStarted && !mIsNewBugReport) {
+            mShowBugReportsButton.setVisibility(View.GONE);
+            return;
+        }
+        if (event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED
+                || event.eventValue == CarDrivingStateEvent.DRIVING_STATE_IDLING) {
             mShowBugReportsButton.setVisibility(View.VISIBLE);
         } else {
             mShowBugReportsButton.setVisibility(View.GONE);
@@ -210,6 +246,43 @@
         }
     }
 
+    private void prepareUi() {
+        if (mSubmitBugReportLayout != null) {
+            return;
+        }
+        setContentView(R.layout.bug_report_activity);
+
+        // Connect to the services here, because they are used only when showing the dialog.
+        // We need to minimize system state change when performing SILENT bug report.
+        mConfig = new Config();
+        mConfig.start();
+        mCar = Car.createCar(this, mCarServiceConnection);
+        mCar.connect();
+
+        mInProgressTitleText = findViewById(R.id.in_progress_title_text);
+        mProgressBar = findViewById(R.id.progress_bar);
+        mProgressText = findViewById(R.id.progress_text);
+        mAddAudioText = findViewById(R.id.bug_report_add_audio_to_existing);
+        mVoiceRecordingView = findViewById(R.id.voice_recording_view);
+        mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view);
+        mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout);
+        mInProgressLayout = findViewById(R.id.in_progress_layout);
+        mShowBugReportsButton = findViewById(R.id.button_show_bugreports);
+        mSubmitButton = findViewById(R.id.button_submit);
+
+        mShowBugReportsButton.setOnClickListener(this::buttonShowBugReportsClick);
+        mSubmitButton.setOnClickListener(this::buttonSubmitClick);
+        findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick);
+        findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick);
+
+        if (mIsNewBugReport) {
+            mSubmitButton.setText(R.string.bugreport_dialog_submit);
+        } else {
+            mSubmitButton.setText(mConfig.getAutoUpload()
+                    ? R.string.bugreport_dialog_upload : R.string.bugreport_dialog_save);
+        }
+    }
+
     private void showInProgressUi() {
         mSubmitBugReportLayout.setVisibility(View.GONE);
         mInProgressLayout.setVisibility(View.VISIBLE);
@@ -231,7 +304,6 @@
         mShowBugReportsButton.setVisibility(View.GONE);
         if (mDrivingStateManager != null) {
             try {
-                // Call onCarDrivingStateChanged(), because it's not called when Car is connected.
                 onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState());
             } catch (CarNotConnectedException e) {
                 Log.e(TAG, "Failed to get current driving state.", e);
@@ -244,28 +316,89 @@
      *
      * <p>This method expected to be called when the activity is started and bound to the service.
      */
-    private void startAudioMessageRecording() {
-        mService.setBugReportProgressListener(this::onProgressChanged);
+    private void onActivityStartedWithBugReportServiceBound() {
+        if (mIsOnActivityStartedWithBugReportServiceBoundCalled) {
+            return;
+        }
+        mIsOnActivityStartedWithBugReportServiceBoundCalled = true;
 
         if (mService.isCollectingBugReport()) {
             Log.i(TAG, "Bug report is already being collected.");
+            mService.setBugReportProgressListener(this::onProgressChanged);
+            prepareUi();
             showInProgressUi();
             return;
         }
 
+        if (ACTION_START_SILENT.equals(getIntent().getAction())) {
+            Log.i(TAG, "Starting a silent bugreport.");
+            MetaBugReport bugReport = createBugReport(this, MetaBugReport.TYPE_SILENT);
+            startBugReportCollection(bugReport);
+            finish();
+            return;
+        }
+
+        // Close the notification shade and other dialogs when showing BugReportActivity dialog.
+        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+        if (ACTION_ADD_AUDIO.equals(getIntent().getAction())) {
+            addAudioToExistingBugReport(
+                    getIntent().getIntExtra(EXTRA_BUGREPORT_ID, /* defaultValue= */ -1));
+            return;
+        }
+
+        Log.i(TAG, "Starting an interactive bugreport.");
+        createNewBugReportWithAudioMessage();
+    }
+
+    private void addAudioToExistingBugReport(int bugreportId) {
+        MetaBugReport bug = BugStorageUtils.findBugReport(this, bugreportId).orElseThrow(
+                () -> new RuntimeException("Failed to find bug report with id " + bugreportId));
+        Log.i(TAG, "Adding audio to the existing bugreport " + bug.getTimestamp());
+        if (bug.getStatus() != Status.STATUS_AUDIO_PENDING.getValue()) {
+            Log.e(TAG, "Failed to add audio, bad status, expected "
+                    + Status.STATUS_AUDIO_PENDING.getValue() + ", got " + bug.getStatus());
+            finish();
+        }
+        File audioFile;
+        try {
+            audioFile = File.createTempFile("audio", "mp3", getCacheDir());
+        } catch (IOException e) {
+            throw new RuntimeException("failed to create temp audio file");
+        }
+        startAudioMessageRecording(/* isNewBugReport= */ false, bug, audioFile);
+    }
+
+    private void createNewBugReportWithAudioMessage() {
+        MetaBugReport bug = createBugReport(this, MetaBugReport.TYPE_INTERACTIVE);
+        startAudioMessageRecording(
+                /* isNewBugReport= */ true,
+                bug,
+                FileUtils.getFileWithSuffix(this, bug.getTimestamp(), "-message.3gp"));
+    }
+
+    /** Shows a dialog UI and starts recording audio message. */
+    private void startAudioMessageRecording(
+            boolean isNewBugReport, MetaBugReport bug, File audioFile) {
         if (mAudioRecordingStarted) {
             Log.i(TAG, "Audio message recording is already started.");
             return;
         }
-
         mAudioRecordingStarted = true;
+        mAudioManager = getSystemService(AudioManager.class);
+        mIsNewBugReport = isNewBugReport;
+        mMetaBugReport = bug;
+        mAudioFile = audioFile;
+        prepareUi();
         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);
+        if (isNewBugReport) {
+            mAddAudioText.setVisibility(View.GONE);
+        } else {
+            mAddAudioText.setVisibility(View.VISIBLE);
+            mAddAudioText.setText(String.format(
+                    getString(R.string.bugreport_dialog_add_audio_to_existing),
+                    mMetaBugReport.getTimestamp()));
+        }
 
         if (!hasRecordPermissions()) {
             requestRecordPermissions();
@@ -282,10 +415,17 @@
             return;
         }
         stopAudioRecording();
-        File tempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp());
-        new DeleteDirectoryAsyncTask().execute(tempDir);
-        BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_USER_CANCELLED, "");
-        Log.i(TAG, "Bug report is cancelled");
+        if (mIsNewBugReport) {
+            // The app creates a temp dir only for new INTERACTIVE bugreports.
+            File tempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp());
+            new DeleteFilesAndDirectoriesAsyncTask().execute(tempDir);
+        } else {
+            BugStorageUtils.deleteBugReportFiles(this, mMetaBugReport.getId());
+            new DeleteFilesAndDirectoriesAsyncTask().execute(mAudioFile);
+        }
+        BugStorageUtils.setBugReportStatus(
+                this, mMetaBugReport, Status.STATUS_USER_CANCELLED, "");
+        Log.i(TAG, "Bug report " + mMetaBugReport.getTimestamp() + " is cancelled");
         mAudioRecordingStarted = false;
     }
 
@@ -294,36 +434,46 @@
     }
 
     private void buttonSubmitClick(View view) {
-        startBugReportingInService();
+        stopAudioRecording();
+        mIsSubmitButtonClicked = true;
+        if (mIsNewBugReport) {
+            Log.i(TAG, "Starting bugreport service.");
+            startBugReportCollection(mMetaBugReport);
+        } else {
+            Log.i(TAG, "Adding audio file to the bugreport " + mMetaBugReport.getTimestamp());
+            new AddAudioToBugReportAsyncTask(this, mConfig, mMetaBugReport, mAudioFile).execute();
+        }
+        setResult(Activity.RESULT_OK);
         finish();
     }
 
+    /** Starts the {@link BugReportService} to collect bug report. */
+    private void startBugReportCollection(MetaBugReport bug) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(BugReportService.EXTRA_META_BUG_REPORT, bug);
+        Intent intent = new Intent(this, BugReportService.class);
+        intent.putExtras(bundle);
+        startForegroundService(intent);
+    }
+
     /**
      * Starts {@link BugReportInfoActivity} and finishes current activity, so it won't be running
-     * in the background and closing {@link BugReportInfoActivity} will not open it again.
+     * in the background and closing {@link BugReportInfoActivity} will not open the current
+     * activity again.
      */
     private void buttonShowBugReportsClick(View view) {
+        // First cancel the audio recording, then delete the bug report from database.
         cancelAudioMessageRecording();
         // Delete the bugreport from database, otherwise pressing "Show Bugreports" button will
         // create unnecessary cancelled bugreports.
         if (mMetaBugReport != null) {
-            BugStorageUtils.deleteBugReport(this, mMetaBugReport.getId());
+            BugStorageUtils.completeDeleteBugReport(this, mMetaBugReport.getId());
         }
         Intent intent = new Intent(this, BugReportInfoActivity.class);
         startActivity(intent);
         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);
@@ -343,7 +493,9 @@
         for (int i = 0; i < grantResults.length; i++) {
             if (Manifest.permission.RECORD_AUDIO.equals(permissions[i])
                     && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
-                startRecordingWithPermission();
+                // Start recording from UI thread, otherwise when MediaRecord#start() fails,
+                // stack trace gets confusing.
+                mHandler.post(this::startRecordingWithPermission);
                 return;
             }
         }
@@ -361,9 +513,7 @@
     }
 
     private void startRecordingWithPermission() {
-        File recordingFile = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
-                "-message.3gp");
-        Log.i(TAG, "Started voice recording, and saving audio to " + recordingFile);
+        Log.i(TAG, "Started voice recording, and saving audio to " + mAudioFile);
 
         mLastAudioFocusRequest = new AudioFocusRequest.Builder(
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
@@ -388,12 +538,12 @@
                 Log.i(TAG, "OnMediaRecorderInfo: what=" + what + ", extra=" + extra));
         mRecorder.setOnErrorListener((MediaRecorder recorder, int what, int extra) ->
                 Log.i(TAG, "OnMediaRecorderError: what=" + what + ", extra=" + extra));
-        mRecorder.setOutputFile(recordingFile);
+        mRecorder.setOutputFile(mAudioFile);
 
         try {
             mRecorder.prepare();
         } catch (IOException e) {
-            Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + recordingFile, e);
+            Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + mAudioFile, e);
             finish();
             return;
         }
@@ -401,19 +551,21 @@
         mRecorder.start();
         mVoiceRecordingView.setRecorder(mRecorder);
 
+        // Messages with token mRecorder are cleared when the activity finishes or recording stops.
         mHandler.postDelayed(() -> {
             Log.i(TAG, "Timed out while recording voice message, cancelling.");
             stopAudioRecording();
             showSubmitBugReportUi(/* isRecording= */ false);
-        }, VOICE_MESSAGE_MAX_DURATION_MILLIS);
+        }, /* token= */ mRecorder, VOICE_MESSAGE_MAX_DURATION_MILLIS);
     }
 
     private void stopAudioRecording() {
         if (mRecorder != null) {
             Log.i(TAG, "Recording ended, stopping the MediaRecorder.");
+            mHandler.removeCallbacksAndMessages(/* token= */ mRecorder);
             try {
                 mRecorder.stop();
-            } catch (IllegalStateException e) {
+            } catch (RuntimeException e) {
                 // Sometimes MediaRecorder doesn't start and stopping it throws an error.
                 // We just log these cases, no need to crash the app.
                 Log.w(TAG, "Couldn't stop media recorder", e);
@@ -430,11 +582,24 @@
         mVoiceRecordingView.setRecorder(null);
     }
 
-    private String getCurrentUserName() {
-        UserManager um = UserManager.get(this);
+    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}.
+     */
+    static MetaBugReport createBugReport(Context context, int type) {
+        String timestamp = MetaBugReport.toBugReportTimestamp(new Date());
+        String username = getCurrentUserName(context);
+        String title = BugReportTitleGenerator.generateBugReportTitle(timestamp, 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. */
@@ -442,17 +607,14 @@
                 new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P',
                         'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'};
 
-        private static final int LOOKUP_STRING_LENGTH = 6;
-
         /**
          * Generates a bugreport title from given timestamp and username.
          *
          * <p>Example: "[A45E8] Feedback from user Driver at 2019-09-21_12:00:00"
          */
-        static String generateBugReportTitle(Date initiatedAt, String username) {
+        static String generateBugReportTitle(String timestamp, String username) {
             // Lookup string is used to search a bug in Buganizer (see b/130915969).
             String lookupString = generateRandomString(LOOKUP_STRING_LENGTH);
-            String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
             return "[" + lookupString + "] Feedback from user " + username + " at " + timestamp;
         }
 
@@ -467,15 +629,66 @@
         }
     }
 
-    /** AsyncTask that recursively deletes directories. */
-    private static class DeleteDirectoryAsyncTask extends AsyncTask<File, Void, Void> {
+    /** AsyncTask that recursively deletes files and directories. */
+    private static class DeleteFilesAndDirectoriesAsyncTask extends AsyncTask<File, Void, Void> {
         @Override
         protected Void doInBackground(File... files) {
             for (File file : files) {
                 Log.i(TAG, "Deleting " + file.getAbsolutePath());
-                FileUtils.deleteDirectory(file);
+                if (file.isFile()) {
+                    file.delete();
+                } else {
+                    FileUtils.deleteDirectory(file);
+                }
             }
             return null;
         }
     }
+
+    /**
+     * AsyncTask that moves audio file to the system user's {@link FileUtils#getPendingDir} and
+     * sets status to either STATUS_UPLOAD_PENDING or STATUS_PENDING_USER_ACTION.
+     */
+    private static class AddAudioToBugReportAsyncTask extends AsyncTask<Void, Void, Void> {
+        private final Context mContext;
+        private final Config mConfig;
+        private final File mAudioFile;
+        private final MetaBugReport mOriginalBug;
+
+        AddAudioToBugReportAsyncTask(
+                Context context, Config config, MetaBugReport bug, File audioFile) {
+            mContext = context;
+            mConfig = config;
+            mOriginalBug = bug;
+            mAudioFile = audioFile;
+        }
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            String audioFileName = FileUtils.getAudioFileName(
+                    MetaBugReport.toBugReportTimestamp(new Date()), mOriginalBug);
+            MetaBugReport bug = BugStorageUtils.update(mContext,
+                    mOriginalBug.toBuilder().setAudioFileName(audioFileName).build());
+            try (OutputStream out = BugStorageUtils.openAudioMessageFileToWrite(mContext, bug);
+                 InputStream input = new FileInputStream(mAudioFile)) {
+                ByteStreams.copy(input, out);
+            } catch (IOException e) {
+                BugStorageUtils.setBugReportStatus(mContext, bug,
+                        com.google.android.car.bugreport.Status.STATUS_WRITE_FAILED,
+                        "Failed to write audio to bug report");
+                Log.e(TAG, "Failed to write audio to bug report", e);
+                return null;
+            }
+            if (mConfig.getAutoUpload()) {
+                BugStorageUtils.setBugReportStatus(mContext, bug,
+                        com.google.android.car.bugreport.Status.STATUS_UPLOAD_PENDING, "");
+            } else {
+                BugStorageUtils.setBugReportStatus(mContext, bug,
+                        com.google.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION, "");
+                BugReportService.showBugReportFinishedNotification(mContext, bug);
+            }
+            mAudioFile.delete();
+            return null;
+        }
+    }
 }
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..343a52d 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;
@@ -33,10 +37,16 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
+
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
@@ -47,105 +57,26 @@
 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;
 
+    /** Used to start {@link BugReportActivity} to add audio message. */
+    private static final int ADD_AUDIO_MESSAGE_REQUEST_CODE = 2;
+
     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;
+    private Config mConfig;
+    private boolean mAudioRecordingStarted;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
         super.onCreate(savedInstanceState);
         setContentView(R.layout.bug_report_info_activity);
 
@@ -159,9 +90,13 @@
         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);
+        mConfig = new Config();
+        mConfig.start();
+
+        mBugInfoAdapter = new BugInfoAdapter(this::onBugReportItemClicked, mConfig);
+        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 +110,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,17 +130,26 @@
         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());
+            Log.i(TAG, "Uploading " + bugReport.getTimestamp());
             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());
+            Log.i(TAG, "Moving " + bugReport.getTimestamp());
             mLastSelectedBugReport = bugReport;
+            mLastSelectedBugInfoViewHolder = holder;
             startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),
                     SELECT_DIRECTORY_REQUEST_CODE);
+        } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_ADD_AUDIO) {
+            // Check mAudioRecordingStarted to prevent double click to BUTTON_TYPE_ADD_AUDIO.
+            if (!mAudioRecordingStarted) {
+                mAudioRecordingStarted = true;
+                startActivityForResult(BugReportActivity.buildAddAudioIntent(this, bugReport),
+                        ADD_AUDIO_MESSAGE_REQUEST_CODE);
+            }
         } else {
             throw new IllegalStateException("unreachable");
         }
@@ -211,11 +164,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 +192,161 @@
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         startActivity(intent);
     }
+
+    /**
+     * Print the Provider's state into the given stream. This gets invoked if
+     * you run "adb shell dumpsys activity BugReportInfoActivity".
+     *
+     * @param prefix Desired prefix to prepend at each line of output.
+     * @param fd The raw file descriptor that the dump is being sent to.
+     * @param writer The PrintWriter to which you should dump your state.  This will be
+     * closed for you after you return.
+     * @param args additional arguments to the dump request.
+     */
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        super.dump(prefix, fd, writer, args);
+        mConfig.dump(prefix, writer);
+    }
+
+    /**
+     * Moves bugreport zip to USB drive and updates RecyclerView.
+     *
+     * <p>It merges bugreport zip file and audio file into one final zip file and moves it.
+     */
+    private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, MetaBugReport> {
+        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;
+        private final ContentResolver mResolver;
+
+        AsyncMoveFilesTask(BugReportInfoActivity activity, BugInfoAdapter bugInfoAdapter,
+                MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder,
+                Uri destinationDir) {
+            mActivity = activity;
+            mBugInfoAdapter = bugInfoAdapter;
+            mBugReport = bugReport;
+            mBugViewHolder = holder;
+            mDestinationDirUri = destinationDir;
+            mResolver = mActivity.getContentResolver();
+        }
+
+        /** Moves the bugreport to the USB drive and returns the updated {@link MetaBugReport}. */
+        @Override
+        protected MetaBugReport doInBackground(Void... params) {
+            try {
+                return copyFilesToUsb();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to copy bugreport "
+                        + mBugReport.getTimestamp() + " to USB", e);
+                return BugStorageUtils.setBugReportStatus(
+                    mActivity, mBugReport,
+                    com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, e);
+            }
+        }
+
+        private MetaBugReport copyFilesToUsb() throws IOException {
+            String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
+            Uri parentDocumentUri =
+                    DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
+            if (!Strings.isNullOrEmpty(mBugReport.getFilePath())) {
+                // There are still old bugreports with deprecated filePath.
+                Uri sourceUri = BugStorageProvider.buildUriWithSegment(
+                        mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE);
+                copyFileToUsb(
+                        new File(mBugReport.getFilePath()).getName(), sourceUri, parentDocumentUri);
+            } else {
+                Uri sourceBugReport = BugStorageProvider.buildUriWithSegment(
+                        mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE);
+                copyFileToUsb(
+                        mBugReport.getBugReportFileName(), sourceBugReport, parentDocumentUri);
+                Uri sourceAudio = BugStorageProvider.buildUriWithSegment(
+                        mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE);
+                copyFileToUsb(mBugReport.getAudioFileName(), sourceAudio, parentDocumentUri);
+            }
+            Log.d(TAG, "Deleting local bug report files.");
+            BugStorageUtils.deleteBugReportFiles(mActivity, mBugReport.getId());
+            return BugStorageUtils.setBugReportStatus(mActivity, mBugReport,
+                    com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL,
+                    "Moved to: " + mDestinationDirUri.getPath());
+        }
+
+        private void copyFileToUsb(String filename, Uri sourceUri, Uri parentDocumentUri)
+                throws IOException {
+            String mimeType = mResolver.getType(sourceUri);
+            Uri newFileUri = DocumentsContract.createDocument(
+                    mResolver, parentDocumentUri, mimeType, filename);
+            if (newFileUri == null) {
+                throw new IOException("Unable to create a file " + filename + " in USB");
+            }
+            try (InputStream input = mResolver.openInputStream(sourceUri);
+                 AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(newFileUri, "w")) {
+                OutputStream output = fd.createOutputStream();
+                ByteStreams.copy(input, output);
+                // Force sync the written data from memory to the disk.
+                fd.getFileDescriptor().sync();
+            }
+        }
+
+        @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);
+        }
+    }
+
+    /** 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();
+        }
+    }
 }
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..3f89660 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -27,7 +27,12 @@
 import android.car.Car;
 import android.car.CarBugreportManager;
 import android.car.CarNotConnectedException;
+import android.content.Context;
 import android.content.Intent;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -38,33 +43,32 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
 import com.google.common.util.concurrent.AtomicDouble;
 
-import libcore.io.IoUtils;
-
 import java.io.BufferedOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Enumeration;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
 /**
  * Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs.
  *
- * <p>After collecting all the logs it updates the {@link MetaBugReport} using {@link
- * BugStorageProvider}, which in turn schedules bug report to upload.
+ * <p>After collecting all the logs it sets the {@link MetaBugReport} status to
+ * {@link Status#STATUS_AUDIO_PENDING} or {@link Status#STATUS_PENDING_USER_ACTION} depending
+ * on {@link MetaBugReport#getType}.
+ *
+ * <p>If the service 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 BugReportService extends Service {
     private static final String TAG = BugReportService.class.getSimpleName();
@@ -74,6 +78,10 @@
      */
     static final String EXTRA_META_BUG_REPORT = "meta_bug_report";
 
+    /** Starts silent (no audio message recording) bugreporting. */
+    private static final String ACTION_START_SILENT =
+            "com.google.android.car.bugreport.action.START_SILENT";
+
     // Wait a short time before starting to capture the bugreport and the screen, so that
     // bugreport activity can detach from the view tree.
     // It is ugly to have a timeout, but it is ok here because such a delay should not really
@@ -81,6 +89,12 @@
     // this, the best option is probably to wait for onDetach events from view tree.
     private static final int ACTIVITY_FINISH_DELAY_MILLIS = 1000;
 
+    /**
+     * Wait a short time before showing "bugreport started" toast message, because the service
+     * will take a screenshot of the screen.
+     */
+    private static final int BUGREPORT_STARTED_TOAST_DELAY_MILLIS = 2000;
+
     private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -90,9 +104,10 @@
     /** Notifications on this channel will pop-up. */
     private static final String STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL";
 
+    /** Persistent notification is shown when bugreport is in progress or waiting for audio. */
     private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
 
-    /** The notification is shown when bugreport is collected. */
+    /** Dismissible notification is shown when bugreport is collected. */
     static final int BUGREPORT_FINISHED_NOTIF_ID = 2;
 
     private static final String OUTPUT_ZIP_FILE = "output_file.zip";
@@ -119,9 +134,16 @@
     private Car mCar;
     private CarBugreportManager mBugreportManager;
     private CarBugreportManager.CarBugreportManagerCallback mCallback;
+    private Config mConfig;
 
     /** A handler on the main thread. */
     private Handler mHandler;
+    /**
+     * A handler to the main thread to show toast messages, it will be cleared when the service
+     * finishes. We need to clear it otherwise when bugreport fails, it will show "bugreport start"
+     * toast, which will confuse users.
+     */
+    private Handler mHandlerToast;
 
     /** A listener that's notified when bugreport progress changes. */
     interface BugReportProgressListener {
@@ -141,7 +163,7 @@
         }
     }
 
-    /** A handler on a main thread. */
+    /** A handler on the main thread. */
     private class BugReportHandler extends Handler {
         @Override
         public void handleMessage(Message message) {
@@ -161,6 +183,8 @@
 
     @Override
     public void onCreate() {
+        Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
         mNotificationManager = getSystemService(NotificationManager.class);
         mNotificationManager.createNotificationChannel(new NotificationChannel(
                 PROGRESS_CHANNEL_ID,
@@ -172,34 +196,58 @@
                 NotificationManager.IMPORTANCE_HIGH));
         mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
         mHandler = new BugReportHandler();
+        mHandlerToast = new Handler();
+        mConfig = new Config();
+        mConfig.start();
+        // Synchronously connect to the car service.
         mCar = Car.createCar(this);
         try {
             mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
         } catch (CarNotConnectedException | NoClassDefFoundError e) {
-            Log.w(TAG, "Couldn't get CarBugreportManager", e);
+            throw new IllegalStateException("Failed to get CarBugreportManager.", e);
         }
     }
 
     @Override
+    public void onDestroy() {
+        if (DEBUG) {
+            Log.d(TAG, "Service destroyed");
+        }
+        mCar.disconnect();
+    }
+
+    @Override
     public int onStartCommand(final Intent intent, int flags, int startId) {
-        if (mIsCollectingBugReport.get()) {
+        if (mIsCollectingBugReport.getAndSet(true)) {
             Log.w(TAG, "bug report is already being collected, ignoring");
             Toast.makeText(this, R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
             return START_NOT_STICKY;
         }
+
         Log.i(TAG, String.format("Will start collecting bug report, version=%s",
                 getPackageVersion(this)));
-        mIsCollectingBugReport.set(true);
+
+        if (ACTION_START_SILENT.equals(intent.getAction())) {
+            Log.i(TAG, "Starting a silent bugreport.");
+            mMetaBugReport = BugReportActivity.createBugReport(this, MetaBugReport.TYPE_SILENT);
+        } else {
+            Bundle extras = intent.getExtras();
+            mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
+        }
+
         mBugReportProgress.set(0);
 
         startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification());
         showProgressNotification();
 
-        Bundle extras = intent.getExtras();
-        mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
-
         collectBugReport();
 
+        // Show a short lived "bugreport started" toast message after a short delay.
+        mHandlerToast.postDelayed(() -> {
+            Toast.makeText(this,
+                    getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show();
+        }, BUGREPORT_STARTED_TOAST_DELAY_MILLIS);
+
         // If the service process gets killed due to heavy memory pressure, do not restart.
         return START_NOT_STICKY;
     }
@@ -213,6 +261,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 +271,7 @@
                 .setCategory(Notification.CATEGORY_STATUS)
                 .setOngoing(true)
                 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false)
+                .setContentIntent(startBugReportInfoActivity)
                 .build();
     }
 
@@ -266,9 +318,14 @@
         Log.i(TAG, "Grabbing bt snoop log");
         File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
                 "-btsnoop.bin.log");
-        try {
-            copyBinaryStream(new FileInputStream(new File(BT_SNOOP_LOG_LOCATION)),
-                    new FileOutputStream(result));
+        File snoopFile = new File(BT_SNOOP_LOG_LOCATION);
+        if (!snoopFile.exists()) {
+            Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping");
+            return;
+        }
+        try (FileInputStream input = new FileInputStream(snoopFile);
+             FileOutputStream output = new FileOutputStream(result)) {
+            ByteStreams.copy(input, output);
         } catch (IOException e) {
             // this regularly happens when snooplog is not enabled so do not log as an error
             Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
@@ -307,14 +364,21 @@
         mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
             @Override
             public void onError(int errorCode) {
-                Log.e(TAG, "Bugreport failed " + errorCode);
-                showToast(R.string.toast_status_failed);
-                // TODO(b/133520419): show this error on Info page or add to zip file.
-                scheduleZipTask();
+                Log.e(TAG, "CarBugreportManager failed: " + errorCode);
                 // We let the UI know that bug reporting is finished, because the next step is to
                 // zip everything and upload.
                 mBugReportProgress.set(MAX_PROGRESS_VALUE);
                 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
+                showToast(R.string.toast_status_failed);
+                BugStorageUtils.setBugReportStatus(
+                        BugReportService.this, mMetaBugReport,
+                        Status.STATUS_WRITE_FAILED, "CarBugreportManager failed: " + errorCode);
+                mIsCollectingBugReport.set(false);
+                mHandler.post(() -> {
+                    mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
+                    stopForeground(true);
+                });
+                mHandlerToast.removeCallbacksAndMessages(null);
             }
 
             @Override
@@ -325,86 +389,102 @@
 
             @Override
             public void onFinished() {
-                Log.i(TAG, "Bugreport finished");
-                scheduleZipTask();
+                Log.d(TAG, "CarBugreportManager finished");
                 mBugReportProgress.set(MAX_PROGRESS_VALUE);
                 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
+                mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus);
             }
         };
         mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
     }
 
-    private void scheduleZipTask() {
-        mSingleThreadExecutor.submit(this::zipDirectoryAndScheduleForUpload);
-    }
-
     /**
      * Shows a clickable bugreport finished notification. When clicked it opens
      * {@link BugReportInfoActivity}.
      */
-    private void showBugReportFinishedNotification() {
-        Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
+    static void showBugReportFinishedNotification(Context context, MetaBugReport bug) {
+        Intent intent = new Intent(context, BugReportInfoActivity.class);
         PendingIntent startBugReportInfoActivity =
-                PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
+                PendingIntent.getActivity(context, 0, intent, 0);
         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))
+                .Builder(context, STATUS_CHANNEL_ID)
+                .setContentTitle(context.getText(R.string.notification_bugreport_finished_title))
+                .setContentText(bug.getTitle())
                 .setCategory(Notification.CATEGORY_STATUS)
                 .setSmallIcon(R.drawable.ic_upload)
                 .setContentIntent(startBugReportInfoActivity)
                 .build();
-        mNotificationManager.notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
+        context.getSystemService(NotificationManager.class)
+                .notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
     }
 
-    private void zipDirectoryAndScheduleForUpload() {
+    /**
+     * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and
+     * updates the bug report status.
+     *
+     * <p>For {@link MetaBugReport#TYPE_INTERACTIVE}: Sets status to either STATUS_UPLOAD_PENDING or
+     * STATUS_PENDING_USER_ACTION and shows a regular notification.
+     *
+     * <p>For {@link MetaBugReport#TYPE_SILENT}: Sets status to STATUS_AUDIO_PENDING and shows
+     * a dialog to record audio message.
+     */
+    private void zipDirectoryAndUpdateStatus() {
         try {
-            // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
-            // schedules an upload job.
-            zipDirectoryToOutputStream(
-                    FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
-                    BugStorageUtils.openBugReportFile(this, mMetaBugReport));
-            showBugReportFinishedNotification();
+            // All the generated zip files, images and audio messages are located in this dir.
+            // This is located under the current user.
+            String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport);
+            Log.d(TAG, "Zipping bugreport into " + bugreportFileName);
+            mMetaBugReport = BugStorageUtils.update(this,
+                    mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build());
+            File bugReportTempDir = FileUtils.createTempDir(this, mMetaBugReport.getTimestamp());
+            zipDirectoryToOutputStream(bugReportTempDir,
+                    BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport));
+            mIsCollectingBugReport.set(false);
         } catch (IOException e) {
             Log.e(TAG, "Failed to zip files", e);
             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
                     MESSAGE_FAILURE_ZIP);
             showToast(R.string.toast_status_failed);
+            return;
         }
-        mIsCollectingBugReport.set(false);
-        showToast(R.string.toast_status_finished);
-        mHandler.post(() -> stopForeground(true));
+        if (mMetaBugReport.getType() == MetaBugReport.TYPE_SILENT) {
+            BugStorageUtils.setBugReportStatus(BugReportService.this,
+                    mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ "");
+            playNotificationSound();
+            startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport));
+        } else {
+            // NOTE: If bugreport type is INTERACTIVE, it will already contain an audio message.
+            Status status = mConfig.getAutoUpload()
+                    ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION;
+            BugStorageUtils.setBugReportStatus(BugReportService.this,
+                    mMetaBugReport, status, /* message= */ "");
+            showBugReportFinishedNotification(this, mMetaBugReport);
+        }
+        mHandler.post(() -> {
+            mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
+            stopForeground(true);
+        });
+        mHandlerToast.removeCallbacksAndMessages(null);
     }
 
-    @Override
-    public void onDestroy() {
-        if (DEBUG) {
-            Log.d(TAG, "Service destroyed");
+    private void playNotificationSound() {
+        Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+        Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification);
+        if (ringtone == null) {
+            Log.w(TAG, "No notification ringtone found.");
+            return;
         }
-    }
-
-    private static void copyBinaryStream(InputStream in, OutputStream out) throws IOException {
-        OutputStream writer = null;
-        InputStream reader = null;
-        try {
-            writer = new DataOutputStream(out);
-            reader = new DataInputStream(in);
-            rawCopyStream(writer, reader);
-        } finally {
-            IoUtils.closeQuietly(reader);
-            IoUtils.closeQuietly(writer);
+        float volume = ringtone.getVolume();
+        // Use volume from audio manager, otherwise default ringtone volume can be too loud.
+        AudioManager audioManager = getSystemService(AudioManager.class);
+        if (audioManager != null) {
+            int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
+            int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
+            volume = (currentVolume + 0.0f) / maxVolume;
         }
-    }
-
-    // does not close the reader or writer.
-    private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
-        int read;
-        byte[] buf = new byte[8192];
-        while ((read = reader.read(buf, 0, buf.length)) > 0) {
-            writer.write(buf, 0, read);
-        }
+        Log.v(TAG, "Using volume " + volume);
+        ringtone.setVolume(volume);
+        ringtone.play();
     }
 
     /**
@@ -425,59 +505,23 @@
         Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
 
         File[] listFiles = dirToZip.listFiles();
-        ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream));
-        try {
+        try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) {
             for (File file : listFiles) {
                 if (file.isDirectory()) {
                     continue;
                 }
-                if (file.length() == 0) {
-                    // If there were issues with reading from dumpstate socket, the dumpstate zip
-                    // file still might be available in
-                    // /data/user_de/0/com.android.shell/files/bugreports/.
-                    Log.w(TAG, "File " + file.getName() + " is empty, skipping.");
-                    return;
-                }
                 String filename = file.getName();
-
                 // only for the zipped output file, we add individual entries to zip file.
                 if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) {
-                    extractZippedFileToOutputStream(file, zipStream);
+                    ZipUtils.extractZippedFileToZipStream(file, zipStream);
                 } else {
-                    try (FileInputStream reader = new FileInputStream(file)) {
-                        addFileToOutputStream(filename, reader, zipStream);
-                    }
+                    ZipUtils.addFileToZipStream(file, zipStream);
                 }
             }
         } finally {
-            zipStream.close();
             outStream.close();
         }
         // Zipping successful, now cleanup the temp dir.
         FileUtils.deleteDirectory(dirToZip);
     }
-
-    private void extractZippedFileToOutputStream(File file, ZipOutputStream zipStream)
-            throws IOException {
-        ZipFile zipFile = new ZipFile(file);
-        Enumeration<? extends ZipEntry> entries = zipFile.entries();
-        while (entries.hasMoreElements()) {
-            ZipEntry entry = entries.nextElement();
-            try (InputStream stream = zipFile.getInputStream(entry)) {
-                addFileToOutputStream(entry.getName(), stream, zipStream);
-            }
-        }
-    }
-
-    private void addFileToOutputStream(
-            String filename, InputStream reader, ZipOutputStream zipStream) {
-        ZipEntry entry = new ZipEntry(filename);
-        try {
-            zipStream.putNextEntry(entry);
-            rawCopyStream(zipStream, reader);
-            zipStream.closeEntry();
-        } catch (IOException e) {
-            Log.w(TAG, "Failed to add file " + filename + " to the zip.", e);
-        }
-    }
 }
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..d9e271e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.StringDef;
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
@@ -26,13 +27,19 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
 import android.os.CancellationSignal;
-import android.os.Handler;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
-import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Function;
 
 
 /**
@@ -40,32 +47,76 @@
  * In Android Automotive user 0 runs as the system and all the time, while other users won't once
  * their session ends. This content provider enables bug reports to be uploaded even after
  * user session ends.
+ *
+ * <p>A bugreport constists of two files: bugreport zip file and audio file. Audio file is added
+ * later through notification. {@link SimpleUploaderAsyncTask} merges two files into one zip file
+ * before uploading.
+ *
+ * <p>All files are stored under system user's {@link FileUtils#getPendingDir}.
  */
 public class BugStorageProvider extends ContentProvider {
     private static final String TAG = BugStorageProvider.class.getSimpleName();
 
     private static final String AUTHORITY = "com.google.android.car.bugreport";
     private static final String BUG_REPORTS_TABLE = "bugreports";
-    static final Uri BUGREPORT_CONTENT_URI =
-            Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE);
 
-    static final String COLUMN_ID = "_ID";
-    static final String COLUMN_USERNAME = "username";
-    static final String COLUMN_TITLE = "title";
-    static final String COLUMN_TIMESTAMP = "timestamp";
-    static final String COLUMN_DESCRIPTION = "description";
-    static final String COLUMN_FILEPATH = "filepath";
-    static final String COLUMN_STATUS = "status";
-    static final String COLUMN_STATUS_MESSAGE = "message";
+    /** Deletes files associated with a bug report. */
+    static final String URL_SEGMENT_DELETE_FILES = "deleteZipFile";
+    /** Destructively deletes a bug report. */
+    static final String URL_SEGMENT_COMPLETE_DELETE = "completeDelete";
+    /** Opens bugreport file of a bug report, uses column {@link #COLUMN_BUGREPORT_FILENAME}. */
+    static final String URL_SEGMENT_OPEN_BUGREPORT_FILE = "openBugReportFile";
+    /** Opens audio file of a bug report, uses column {@link #URL_MATCHED_OPEN_AUDIO_FILE}. */
+    static final String URL_SEGMENT_OPEN_AUDIO_FILE = "openAudioFile";
+    /**
+     * Opens final bugreport zip file, uses column {@link #COLUMN_FILEPATH}.
+     *
+     * <p>NOTE: This is the old way of storing final zipped bugreport. In
+     * {@code BugStorageProvider#AUDIO_VERSION} {@link #COLUMN_FILEPATH} is dropped. But there are
+     * still some devices with this field set.
+     */
+    static final String URL_SEGMENT_OPEN_FILE = "openFile";
 
     // 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_FILES = 3;
+    private static final int URL_MATCHED_COMPLETE_DELETE = 4;
+    private static final int URL_MATCHED_OPEN_BUGREPORT_FILE = 5;
+    private static final int URL_MATCHED_OPEN_AUDIO_FILE = 6;
+    private static final int URL_MATCHED_OPEN_FILE = 7;
 
-    private Handler mHandler;
+    @StringDef({
+            URL_SEGMENT_DELETE_FILES,
+            URL_SEGMENT_COMPLETE_DELETE,
+            URL_SEGMENT_OPEN_BUGREPORT_FILE,
+            URL_SEGMENT_OPEN_AUDIO_FILE,
+            URL_SEGMENT_OPEN_FILE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface UriActionSegments {}
+
+    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";
+    static final String COLUMN_TIMESTAMP = "timestamp";
+    /** not used anymore */
+    static final String COLUMN_DESCRIPTION = "description";
+    /** not used anymore, but some devices still might have bugreports with this field set. */
+    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";
+    static final String COLUMN_BUGREPORT_FILENAME = "bugreport_filename";
+    static final String COLUMN_AUDIO_FILENAME = "audio_filename";
 
     private DatabaseHelper mDatabaseHelper;
     private final UriMatcher mUriMatcher;
+    private Config mConfig;
 
     /**
      * A helper class to work with sqlite database.
@@ -78,9 +129,13 @@
         /**
          * All changes in database versions should be recorded here.
          * 1: Initial version.
+         * 2: Add integer column details_needed.
+         * 3: Add string column audio_filename and bugreport_filename.
          */
         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 AUDIO_VERSION = 3;
+        private static final int DATABASE_VERSION = AUDIO_VERSION;
 
         private static final String CREATE_TABLE = "CREATE TABLE " + BUG_REPORTS_TABLE + " ("
                 + COLUMN_ID + " INTEGER PRIMARY KEY,"
@@ -90,7 +145,10 @@
                 + 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 + ","
+                + COLUMN_BUGREPORT_FILENAME + " TEXT DEFAULT NULL,"
+                + COLUMN_AUDIO_FILENAME + " TEXT DEFAULT NULL"
                 + ");";
 
         DatabaseHelper(Context context) {
@@ -105,24 +163,56 @@
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             Log.w(TAG, "Upgrading from " + oldVersion + " to " + newVersion);
+            if (oldVersion < TYPE_VERSION) {
+                db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+                        + COLUMN_TYPE + " INTEGER DEFAULT " + MetaBugReport.TYPE_INTERACTIVE);
+            }
+            if (oldVersion < AUDIO_VERSION) {
+                db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+                        + COLUMN_BUGREPORT_FILENAME + " TEXT DEFAULT NULL");
+                db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+                        + COLUMN_AUDIO_FILENAME + " TEXT DEFAULT NULL");
+            }
         }
     }
 
-    /** Builds {@link Uri} that points to a bugreport entry with provided bugreport id. */
-    static Uri buildUriWithBugId(int bugReportId) {
-        return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/" + bugReportId);
+    /**
+     * Builds an {@link Uri} that points to the single bug report and performs an action
+     * defined by given URI segment.
+     */
+    static Uri buildUriWithSegment(int bugReportId, @UriActionSegments String segment) {
+        return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/"
+                + segment + "/" + 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_FILES + "/#",
+                URL_MATCHED_DELETE_FILES);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_COMPLETE_DELETE + "/#",
+                URL_MATCHED_COMPLETE_DELETE);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_BUGREPORT_FILE + "/#",
+                URL_MATCHED_OPEN_BUGREPORT_FILE);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_AUDIO_FILE + "/#",
+                URL_MATCHED_OPEN_AUDIO_FILE);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_FILE + "/#",
+                URL_MATCHED_OPEN_FILE);
     }
 
     @Override
     public boolean onCreate() {
+        Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
         mDatabaseHelper = new DatabaseHelper(getContext());
-        mHandler = new Handler();
+        mConfig = new Config();
+        mConfig.start();
         return true;
     }
 
@@ -181,10 +271,6 @@
         switch (mUriMatcher.match(uri)) {
             case URL_MATCHED_BUG_REPORTS_URI:
                 table = BUG_REPORTS_TABLE;
-                String filepath = FileUtils.getZipFile(getContext(),
-                        (String) values.get(COLUMN_TIMESTAMP),
-                        (String) values.get(COLUMN_USERNAME)).getPath();
-                values.put(COLUMN_FILEPATH, filepath);
                 break;
             default:
                 throw new IllegalArgumentException("unknown uri" + uri);
@@ -203,31 +289,46 @@
     @Nullable
     @Override
     public String getType(@NonNull Uri uri) {
-        if (mUriMatcher.match(uri) != URL_MATCHED_BUG_REPORT_ID_URI) {
-            throw new IllegalArgumentException("unknown uri:" + uri);
+        switch (mUriMatcher.match(uri)) {
+            case URL_MATCHED_OPEN_BUGREPORT_FILE:
+            case URL_MATCHED_OPEN_FILE:
+                return "application/zip";
+            case URL_MATCHED_OPEN_AUDIO_FILE:
+                return "audio/3gpp";
+            default:
+                throw new IllegalArgumentException("unknown uri:" + uri);
         }
-        // We only store zip files in this provider.
-        return "application/zip";
     }
 
     @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:
+            case URL_MATCHED_DELETE_FILES:
                 if (selection != null || selectionArgs != null) {
                     throw new IllegalArgumentException("selection is not allowed for "
-                            + URL_MATCHED_BUG_REPORT_ID_URI);
+                            + URL_MATCHED_DELETE_FILES);
+                }
+                if (deleteFilesFor(getBugReportFromUri(uri))) {
+                    getContext().getContentResolver().notifyChange(uri, null);
+                    return 1;
+                }
+                return 0;
+            case URL_MATCHED_COMPLETE_DELETE:
+                if (selection != null || selectionArgs != null) {
+                    throw new IllegalArgumentException("selection is not allowed for "
+                            + URL_MATCHED_COMPLETE_DELETE);
                 }
                 selection = COLUMN_ID + " = ?";
                 selectionArgs = new String[]{uri.getLastPathSegment()};
-                break;
+                // Ignore the results of zip file deletion, possibly it wasn't even created.
+                deleteFilesFor(getBugReportFromUri(uri));
+                getContext().getContentResolver().notifyChange(uri, null);
+                return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
             default:
                 throw new IllegalArgumentException("Unknown URL " + uri);
         }
-        SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
-        return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
     }
 
     @Override
@@ -263,77 +364,72 @@
     }
 
     /**
-     * This is called when the OutputStream is requested by
-     * {@link BugStorageUtils#openBugReportFile}.
+     * This is called when a file is opened.
      *
-     * It expects the file to be a zip file and schedules an upload under the primary user.
+     * <p>See {@link BugStorageUtils#openBugReportFileToWrite},
+     * {@link BugStorageUtils#openAudioMessageFileToWrite}.
      */
     @Nullable
     @Override
     public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
             throws FileNotFoundException {
-        if (mUriMatcher.match(uri) != URL_MATCHED_BUG_REPORT_ID_URI) {
-            throw new IllegalArgumentException("unknown uri:" + uri);
+        Function<MetaBugReport, String> fileNameExtractor;
+        switch (mUriMatcher.match(uri)) {
+            case URL_MATCHED_OPEN_BUGREPORT_FILE:
+                fileNameExtractor = MetaBugReport::getBugReportFileName;
+                break;
+            case URL_MATCHED_OPEN_AUDIO_FILE:
+                fileNameExtractor = MetaBugReport::getAudioFileName;
+                break;
+            case URL_MATCHED_OPEN_FILE:
+                File file = new File(getBugReportFromUri(uri).getFilePath());
+                Log.v(TAG, "Opening file " + file + " with mode " + mode);
+                return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
+            default:
+                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.");
-        }
-
+        // URI contains bugreport ID as the last segment, see the matched urls.
+        MetaBugReport bugReport = getBugReportFromUri(uri);
+        File file = new File(
+                FileUtils.getPendingDir(getContext()), fileNameExtractor.apply(bugReport));
+        Log.v(TAG, "Opening file " + file + " with mode " + mode);
         int modeBits = ParcelFileDescriptor.parseMode(mode);
-        try {
-            return ParcelFileDescriptor.open(new File(path), modeBits, mHandler, e -> {
-                if (mode.equals("r")) {
-                    Log.i(TAG, "File " + path + " opened in read-only mode.");
-                    return;
-                } else if (!mode.equals("w")) {
-                    Log.e(TAG, "Only read-only or write-only mode supported; mode=" + mode);
-                    return;
-                }
-                Log.i(TAG, "File " + path + " opened in write-only mode.");
-                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
-                            : Status.STATUS_PENDING_USER_ACTION;
-                } else {
-                    // We log it and ignore it
-                    Log.e(TAG, "Bug report file write failed ", e);
-                    status = Status.STATUS_WRITE_FAILED;
-                }
-                SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
-                ContentValues values = new ContentValues();
-                values.put(COLUMN_STATUS, status.getValue());
-                db.update(BUG_REPORTS_TABLE, values, COLUMN_ID + "=?",
-                        new String[]{ uri.getLastPathSegment() });
-                if (status == Status.STATUS_UPLOAD_PENDING) {
-                    JobSchedulingUtils.scheduleUploadJob(BugStorageProvider.this.getContext());
-                }
-                Log.i(TAG, "Finished adding bugreport " + path + " " + uri);
-            });
-        } catch (IOException e) {
-            // An IOException (for example not being able to open the file, will crash us.
-            // That is ok.
-            throw new RuntimeException(e);
+        return ParcelFileDescriptor.open(file, modeBits);
+    }
+
+    private MetaBugReport getBugReportFromUri(@NonNull Uri uri) {
+        int bugreportId = Integer.parseInt(uri.getLastPathSegment());
+        return BugStorageUtils.findBugReport(getContext(), bugreportId)
+                .orElseThrow(() -> new IllegalArgumentException("No record found for " + uri));
+    }
+
+    /**
+     * Print the Provider's state into the given stream. This gets invoked if
+     * you run "dumpsys activity provider com.google.android.car.bugreport/.BugStorageProvider".
+     *
+     * @param fd The raw file descriptor that the dump is being sent to.
+     * @param writer The PrintWriter to which you should dump your state.  This will be
+     * closed for you after you return.
+     * @param args additional arguments to the dump request.
+     */
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        writer.println("BugStorageProvider:");
+        mConfig.dump(/* prefix= */ "  ", writer);
+    }
+
+    private boolean deleteFilesFor(MetaBugReport bugReport) {
+        if (!Strings.isNullOrEmpty(bugReport.getFilePath())) {
+            // Old bugreports have only filePath.
+            return new File(bugReport.getFilePath()).delete();
         }
+        File pendingDir = FileUtils.getPendingDir(getContext());
+        boolean result = true;
+        if (!Strings.isNullOrEmpty(bugReport.getAudioFileName())) {
+            result = new File(pendingDir, bugReport.getAudioFileName()).delete();
+        }
+        if (!Strings.isNullOrEmpty(bugReport.getBugReportFileName())) {
+            result = result && new File(pendingDir, bugReport.getBugReportFileName()).delete();
+        }
+        return result;
     }
 }
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..a009129 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
@@ -15,12 +15,15 @@
  */
 package com.google.android.car.bugreport;
 
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_AUDIO_FILENAME;
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_BUGREPORT_FILENAME;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_FILEPATH;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_ID;
 import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_STATUS;
 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,18 +33,21 @@
 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;
+import com.google.common.base.Strings;
 
 import java.io.FileNotFoundException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
 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 +74,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,68 +82,95 @@
             @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);
-
-        Cursor c = r.query(uri, new String[]{COLUMN_ID}, null, null, null);
-        int count = (c == null) ? 0 : c.getCount();
-        if (count != 1) {
-            throw new RuntimeException("Could not create a bug report entry.");
-        }
-        c.moveToFirst();
-        int id = getInt(c, COLUMN_ID);
-        c.close();
-        return new MetaBugReport.Builder(id, timestamp)
-                .setTitle(title)
-                .setUserName(username)
-                .build();
+        return findBugReport(context, Integer.parseInt(uri.getLastPathSegment())).get();
     }
 
-    /**
-     * Returns a file stream to write the zipped file to. The content provider listens for file
-     * descriptor to be closed, and as soon as it is closed, {@link BugStorageProvider} schedules
-     * it for upload.
-     *
-     * @param context       - an application context.
-     * @param metaBugReport - a bug report.
-     * @return a file descriptor where a zip content should be written.
-     */
+    /** Returns an output stream to write the zipped file to. */
     @NonNull
-    static OutputStream openBugReportFile(
+    static OutputStream openBugReportFileToWrite(
             @NonNull Context context, @NonNull MetaBugReport metaBugReport)
             throws FileNotFoundException {
         ContentResolver r = context.getContentResolver();
+        return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
+                metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
+    }
 
-        // Write the file. When file is closed, bug report record status
-        // will automatically be made ready for uploading.
-        return r.openOutputStream(BugStorageProvider.buildUriWithBugId(metaBugReport.getId()));
+    /** Returns an output stream to write the audio message file to. */
+    static OutputStream openAudioMessageFileToWrite(
+            @NonNull Context context, @NonNull MetaBugReport metaBugReport)
+            throws FileNotFoundException {
+        ContentResolver r = context.getContentResolver();
+        return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
+                metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
     }
 
     /**
-     * Deletes {@link MetaBugReport} record from a local database. Returns true if the record was
-     * deleted.
+     * Returns an input stream to read the final zip file from.
+     *
+     * <p>NOTE: This is the old way of storing final zipped bugreport. See
+     * {@link BugStorageProvider#URL_SEGMENT_OPEN_FILE} for more info.
+     */
+    static InputStream openFileToRead(Context context, MetaBugReport bug)
+            throws FileNotFoundException {
+        return context.getContentResolver().openInputStream(
+                BugStorageProvider.buildUriWithSegment(
+                        bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE));
+    }
+
+    /** Returns an input stream to read the bug report zip file from. */
+    static InputStream openBugReportFileToRead(Context context, MetaBugReport bug)
+            throws FileNotFoundException {
+        return context.getContentResolver().openInputStream(
+                BugStorageProvider.buildUriWithSegment(
+                        bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
+    }
+
+    /** Returns an input stream to read the audio file from. */
+    static InputStream openAudioFileToRead(Context context, MetaBugReport bug)
+            throws FileNotFoundException {
+        return context.getContentResolver().openInputStream(
+                BugStorageProvider.buildUriWithSegment(
+                        bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
+    }
+
+    /**
+     * Deletes {@link MetaBugReport} record from a local database and deletes the associated file.
+     *
+     * <p>WARNING: destructive operation.
      *
      * @param context     - an application context.
      * @param bugReportId - a bug report id.
      * @return true if the record was deleted.
      */
-    static boolean deleteBugReport(@NonNull Context context, int bugReportId) {
+    static boolean completeDeleteBugReport(@NonNull Context context, int bugReportId) {
         ContentResolver r = context.getContentResolver();
-        return r.delete(BugStorageProvider.buildUriWithBugId(bugReportId), null, null) == 1;
+        return r.delete(BugStorageProvider.buildUriWithSegment(
+                bugReportId, BugStorageProvider.URL_SEGMENT_COMPLETE_DELETE), null, null) == 1;
+    }
+
+    /** Deletes all files for given bugreport id; doesn't delete sqlite3 record. */
+    static boolean deleteBugReportFiles(@NonNull Context context, int bugReportId) {
+        ContentResolver r = context.getContentResolver();
+        return r.delete(BugStorageProvider.buildUriWithSegment(
+                bugReportId, BugStorageProvider.URL_SEGMENT_DELETE_FILES), null, null) == 1;
     }
 
     /**
-     * Returns bugreports that are waiting to be uploaded.
+     * Returns all the bugreports that are waiting to be uploaded.
      */
     @NonNull
-    public static List<MetaBugReport> getPendingBugReports(@NonNull Context context) {
+    public static List<MetaBugReport> getUploadPendingBugReports(@NonNull Context context) {
         String selection = COLUMN_STATUS + "=?";
         String[] selectionArgs = new String[]{
                 Integer.toString(Status.STATUS_UPLOAD_PENDING.getValue())};
@@ -152,17 +186,32 @@
         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,
                 COLUMN_USERNAME,
                 COLUMN_TITLE,
                 COLUMN_TIMESTAMP,
+                COLUMN_BUGREPORT_FILENAME,
+                COLUMN_AUDIO_FILENAME,
                 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);
@@ -171,13 +220,17 @@
 
         if (count > 0) c.moveToFirst();
         for (int i = 0; i < count; i++) {
-            MetaBugReport meta = new MetaBugReport.Builder(getInt(c, COLUMN_ID),
-                    getString(c, COLUMN_TIMESTAMP))
+            MetaBugReport meta = MetaBugReport.builder()
+                    .setId(getInt(c, COLUMN_ID))
+                    .setTimestamp(getString(c, COLUMN_TIMESTAMP))
                     .setUserName(getString(c, COLUMN_USERNAME))
                     .setTitle(getString(c, COLUMN_TITLE))
-                    .setFilepath(getString(c, COLUMN_FILEPATH))
+                    .setBugReportFileName(getString(c, COLUMN_BUGREPORT_FILENAME))
+                    .setAudioFileName(getString(c, COLUMN_AUDIO_FILENAME))
+                    .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();
@@ -207,7 +260,7 @@
             Log.w(TAG, "Column " + colName + " not found.");
             return "";
         }
-        return c.getString(colIndex);
+        return Strings.nullToEmpty(c.getString(colIndex));
     }
 
     /**
@@ -219,13 +272,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.
@@ -244,6 +290,22 @@
         setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_PENDING, msg);
     }
 
+    /**
+     * Sets {@link MetaBugReport} status {@link Status#STATUS_EXPIRED}.
+     * Deletes the associated zip file from disk.
+     *
+     * @return true if succeeded.
+     */
+    static boolean expireBugReport(@NonNull Context context,
+            @NonNull MetaBugReport metaBugReport, @NonNull Instant expiredAt) {
+        metaBugReport = setBugReportStatus(
+                context, metaBugReport, Status.STATUS_EXPIRED, "Expired on " + expiredAt);
+        if (metaBugReport.getStatus() != Status.STATUS_EXPIRED.getValue()) {
+            return false;
+        }
+        return deleteBugReportFiles(context, metaBugReport.getId());
+    }
+
     /** Gets the root cause of the error. */
     @NonNull
     private static String getRootCauseMessage(@Nullable Throwable t) {
@@ -260,18 +322,52 @@
         return t.getMessage();
     }
 
-    /** Updates bug report record status. */
-    static void setBugReportStatus(
+    /**
+     * Updates bug report record status.
+     *
+     * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
+     * schedules the bugreport to be uploaded.
+     *
+     * @return Updated {@link MetaBugReport}.
+     */
+    static MetaBugReport setBugReportStatus(
             Context context, MetaBugReport bugReport, Status status, String message) {
-        // update status
+        return update(context, bugReport.toBuilder()
+                .setStatus(status.getValue())
+                .setStatusMessage(message)
+                .build());
+    }
+
+    /**
+     * Updates bug report record status.
+     *
+     * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
+     * schedules the bugreport to be uploaded.
+     *
+     * @return Updated {@link MetaBugReport}.
+     */
+    static MetaBugReport setBugReportStatus(
+            Context context, MetaBugReport bugReport, Status status, Exception e) {
+        return setBugReportStatus(context, bugReport, status, getRootCauseMessage(e));
+    }
+
+    /**
+     * Updates the bugreport and returns the updated version.
+     *
+     * <p>NOTE: doesn't update all the fields.
+     */
+    static MetaBugReport update(Context context, MetaBugReport bugReport) {
+        // Update only necessary fields.
         ContentValues values = new ContentValues();
-        values.put(COLUMN_STATUS, status.getValue());
-        if (!TextUtils.isEmpty(message)) {
-            values.put(COLUMN_STATUS_MESSAGE, message);
-        }
+        values.put(COLUMN_BUGREPORT_FILENAME, bugReport.getBugReportFileName());
+        values.put(COLUMN_AUDIO_FILENAME, bugReport.getAudioFileName());
+        values.put(COLUMN_STATUS, bugReport.getStatus());
+        values.put(COLUMN_STATUS_MESSAGE, bugReport.getStatusMessage());
         String where = COLUMN_ID + "=" + bugReport.getId();
-        context.getContentResolver().update(BugStorageProvider.BUGREPORT_CONTENT_URI, values,
-                where, null);
+        context.getContentResolver().update(
+                BugStorageProvider.BUGREPORT_CONTENT_URI, values, where, null);
+        return findBugReport(context, bugReport.getId()).orElseThrow(
+                () -> new IllegalArgumentException("Bug " + bugReport.getId() + " not found"));
     }
 
     private static String currentTimestamp() {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java
new file mode 100644
index 0000000..70f65bf
--- /dev/null
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java
@@ -0,0 +1,166 @@
+/*
+ * 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.bugreport;
+
+import android.app.ActivityThread;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+
+/**
+ * Contains config for BugReport App.
+ *
+ * <p>The config is kept synchronized with {@code car} namespace. It's not defined in
+ * {@link DeviceConfig}.
+ *
+ * <ul>To get/set the flags via adb:
+ *   <li>{@code adb shell device_config get car bugreport_upload_destination}
+ *   <li>{@code adb shell device_config put car bugreport_upload_destination gcs}
+ *   <li>{@code adb shell device_config delete car bugreport_upload_destination}
+ * </ul>
+ */
+final class Config {
+    private static final String TAG = Config.class.getSimpleName();
+
+    private static final String HAWK = "hawk";
+
+    /**
+     * Namespace for all Android Automotive related features.
+     *
+     * <p>In the future it will move to {@code DeviceConfig#NAMESPACE_CAR}.
+     */
+    private static final String NAMESPACE_CAR = "car";
+
+    /**
+     * A string flag, can be one of {@code null} or {@link #UPLOAD_DESTINATION_GCS}.
+     */
+    private static final String KEY_BUGREPORT_UPLOAD_DESTINATION = "bugreport_upload_destination";
+
+    /**
+     * A value for {@link #KEY_BUGREPORT_UPLOAD_DESTINATION}.
+     *
+     * Upload bugreports to GCS. Only works in {@code userdebug} or {@code eng} builds.
+     */
+    private static final String UPLOAD_DESTINATION_GCS = "gcs";
+
+    /**
+     * A system property to force enable the app bypassing the {@code userdebug/eng} build check.
+     */
+    private static final String PROP_FORCE_ENABLE = "android.car.bugreport.force_enable";
+
+    /**
+     * Temporary flag to retain the old behavior.
+     *
+     * Default is {@code true}.
+     *
+     * TODO(b/143183993): Disable auto-upload to GCS after testing DeviceConfig.
+     */
+    private static final String ENABLE_AUTO_UPLOAD = "android.car.bugreport.enableautoupload";
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private String mUploadDestination = null;
+
+    void start() {
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_CAR,
+                ActivityThread.currentApplication().getMainExecutor(), this::onPropertiesChanged);
+        updateConstants();
+    }
+
+    private void onPropertiesChanged(DeviceConfig.Properties properties) {
+        if (properties.getKeyset().contains(KEY_BUGREPORT_UPLOAD_DESTINATION)) {
+            updateConstants();
+        }
+    }
+
+    /** Returns true if bugreport app is enabled for this device. */
+    static boolean isBugReportEnabled() {
+        return Build.IS_DEBUGGABLE || SystemProperties.getBoolean(PROP_FORCE_ENABLE, false);
+    }
+
+    /** If new bugreports should be scheduled for uploading. */
+    boolean getAutoUpload() {
+        if (isTempForceAutoUploadGcsEnabled()) {
+            Log.d(TAG, "Enabling auto-upload because ENABLE_AUTO_UPLOAD is true");
+            return true;
+        }
+        // TODO(b/144851443): Enable auto-upload only if upload destination is Gcs until
+        //                    we create a way to allow implementing OEMs custom upload logic.
+        return isUploadDestinationGcs();
+    }
+
+    /**
+     * Returns {@link true} if bugreport upload destination is GCS.
+     */
+    boolean isUploadDestinationGcs() {
+        if (isTempForceAutoUploadGcsEnabled()) {
+            Log.d(TAG, "Setting upload dest to GCS ENABLE_AUTO_UPLOAD is true");
+            return true;
+        }
+        // TODO(b/146214182): Enable uploading to GCS if the device is hawk.
+        if (HAWK.equals(Build.DEVICE) && Build.IS_DEBUGGABLE) {
+            return true;
+        }
+        // NOTE: enable it only for userdebug builds, unless it's force enabled using a system
+        //       property.
+        return UPLOAD_DESTINATION_GCS.equals(getUploadDestination()) && Build.IS_DEBUGGABLE;
+    }
+
+    private static boolean isTempForceAutoUploadGcsEnabled() {
+        return SystemProperties.getBoolean(ENABLE_AUTO_UPLOAD, /* def= */ true);
+    }
+
+    /**
+     * Returns value of a flag {@link #KEY_BUGREPORT_UPLOAD_DESTINATION}.
+     */
+    private String getUploadDestination() {
+        synchronized (mLock) {
+            return mUploadDestination;
+        }
+    }
+
+    private void updateConstants() {
+        synchronized (mLock) {
+            mUploadDestination = DeviceConfig.getString(NAMESPACE_CAR,
+                    KEY_BUGREPORT_UPLOAD_DESTINATION, /* defaultValue= */ null);
+        }
+    }
+
+    void dump(String prefix, PrintWriter pw) {
+        pw.println(prefix + "car.bugreport.Config:");
+
+        pw.print(prefix + "  ");
+        pw.print("getAutoUpload");
+        pw.print("=");
+        pw.println(getAutoUpload() ? "true" : "false");
+
+        pw.print(prefix + "  ");
+        pw.print("getUploadDestination");
+        pw.print("=");
+        pw.println(getUploadDestination());
+
+        pw.print(prefix + "  ");
+        pw.print("isUploadDestinationGcs");
+        pw.print("=");
+        pw.println(isUploadDestinationGcs());
+    }
+}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
index 47d05c6..b30035e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
@@ -17,6 +17,8 @@
 
 import android.content.Context;
 
+import com.google.common.base.Preconditions;
+
 import java.io.File;
 
 /**
@@ -34,14 +36,16 @@
  */
 public class FileUtils {
     private static final String PREFIX = "bugreport-";
-    // bug reports waiting to be uploaded
+    /** A directory under the system user; contains bugreport zip files and audio files. */
     private static final String PENDING_DIR = "bug_reports_pending";
-    // temporary directory, used for zipping files
+    // Temporary directory under the current user, used for zipping files.
     private static final String TEMP_DIR = "bug_reports_temp";
 
     private static final String FS = "@";
 
-    private static File getPendingDir(Context context) {
+    static File getPendingDir(Context context) {
+        Preconditions.checkArgument(context.getUser().isSystem(),
+                "Must be called from the system user.");
         File dir = new File(context.getDataDir(), PENDING_DIR);
         dir.mkdirs();
         return dir;
@@ -62,15 +66,38 @@
      * single file.
      */
     static File getTempDir(Context context, String timestamp) {
+        Preconditions.checkArgument(!context.getUser().isSystem(),
+                "Must be called from the current user.");
         return new File(context.getDataDir(), TEMP_DIR + "/" + timestamp);
     }
 
     /**
-     * Returns zip file directory with the given timestamp and ldap
+     * Constructs a bugreport zip file name.
+     *
+     * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
      */
-    static File getZipFile(Context context, String timestamp, String ldap) {
-        File zipdir = getPendingDir(context);
-        return new File(zipdir, PREFIX + ldap + FS + timestamp + ".zip");
+    static String getZipFileName(MetaBugReport bug) {
+        String lookupCode = extractLookupCode(bug);
+        return PREFIX + bug.getUserName() + FS + bug.getTimestamp() + "-" + lookupCode + ".zip";
+    }
+
+    /**
+     * Constructs a audio message file name.
+     *
+     * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
+     *
+     * @param timestamp - current timestamp, when audio was created.
+     * @param bug       - a bug report.
+     */
+    static String getAudioFileName(String timestamp, MetaBugReport bug) {
+        String lookupCode = extractLookupCode(bug);
+        return PREFIX + bug.getUserName() + FS + timestamp + "-" + lookupCode + "-message.3gp";
+    }
+
+    private static String extractLookupCode(MetaBugReport bug) {
+        Preconditions.checkArgument(bug.getTitle().startsWith("["),
+                "Invalid bugreport title, doesn't contain lookup code. ");
+        return bug.getTitle().substring(1, BugReportActivity.LOOKUP_STRING_LENGTH + 1);
     }
 
     /**
@@ -99,7 +126,6 @@
         return new File(getTempDir(context, timestamp), name);
     }
 
-
     /**
      * Deletes a directory and its contents recursively
      *
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..eac8b9a 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
@@ -19,7 +19,6 @@
 import android.app.job.JobScheduler;
 import android.content.ComponentName;
 import android.content.Context;
-import android.os.SystemProperties;
 import android.util.Log;
 
 /**
@@ -32,15 +31,7 @@
     private static final int RETRY_DELAY_IN_MS = 5_000;
 
     /**
-     * The system property to disable auto-upload when bug reports are collected. When auto-upload
-     * is disabled, the app waits for user action on collected bug reports: user can either
-     * upload to Google Cloud or copy to flash drive.
-     */
-    private static final String PROP_DISABLE_AUTO_UPLOAD =
-            "android.car.bugreport.disableautoupload";
-
-    /**
-     * Schedules an upload job under the current user.
+     * Schedules {@link UploadJob} under the current user.
      *
      * <p>Make sure this method is called under the primary user.
      *
@@ -72,15 +63,4 @@
                 .setBackoffCriteria(RETRY_DELAY_IN_MS, JobInfo.BACKOFF_POLICY_LINEAR)
                 .build());
     }
-
-    /**
-     * Returns true if collected bugreports should be uploaded automatically.
-     *
-     * <p>If it returns false, the app maps to an alternative workflow that requires user action
-     * 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() {
-        return !SystemProperties.getBoolean(PROP_DISABLE_AUTO_UPLOAD, false);
-    }
 }
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..fcdb5b7 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
@@ -15,77 +15,95 @@
  */
 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;
 
-/** Represents the information that a bugreport can contain. */
-public final class MetaBugReport implements Parcelable {
-    private final int mId;
-    private final String mTimestamp;
-    private final String mTitle;
-    private final String mUsername;
-    private final String mFilePath;
-    private final int mStatus;
-    private final String mStatusMessage;
+import com.google.auto.value.AutoValue;
 
-    private MetaBugReport(Builder builder) {
-        mId = builder.mId;
-        mTimestamp = builder.mTimestamp;
-        mTitle = builder.mTitle;
-        mUsername = builder.mUsername;
-        mFilePath = builder.mFilePath;
-        mStatus = builder.mStatus;
-        mStatusMessage = builder.mStatusMessage;
-    }
+import java.lang.annotation.Retention;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/** Represents the information that a bugreport can contain. */
+@AutoValue
+abstract class MetaBugReport implements Parcelable {
+
+    private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+
+    /** The app records audio message when initiated. Can change audio state. */
+    static final int TYPE_INTERACTIVE = 0;
+
+    /**
+     * The app doesn't show dialog and doesn't record audio when initiated. It allows user to
+     * add audio message when bugreport is collected.
+     */
+    static final int TYPE_SILENT = 1;
+
+    /** Annotation for bug report types. */
+    @Retention(SOURCE)
+    @IntDef({TYPE_INTERACTIVE, TYPE_SILENT})
+    @interface BugReportType {};
 
     /**
      * @return Id of the bug report. Bug report id monotonically increases and is unique.
      */
-    public int getId() {
-        return mId;
-    }
+    public abstract int getId();
 
     /**
      * @return Username (LDAP) that created this bugreport
      */
-    public String getUsername() {
-        return mUsername == null ? "" : mUsername;
-    }
+    public abstract String getUserName();
 
     /**
      * @return Title of the bug.
      */
-    public String getTitle() {
-        return mTitle == null ? "" : mTitle;
-    }
+    public abstract String getTitle();
 
     /**
      * @return Timestamp when the bug report is initialized.
      */
-    public String getTimestamp() {
-        return mTimestamp == null ? "" : mTimestamp;
-    }
+    public abstract String getTimestamp();
 
     /**
-     * @return path to the zip file
+     * @return path to the zip file stored under the system user.
+     *
+     * <p>NOTE: This is the old way of storing final zipped bugreport. See
+     * {@link BugStorageProvider#URL_SEGMENT_OPEN_FILE} for more info.
      */
-    public String getFilePath() {
-        return mFilePath == null ? "" : mFilePath;
-    }
+    public abstract String getFilePath();
 
     /**
-     * @return Status of the bug upload.
+     * @return filename of the bug report zip file stored under the system user.
      */
-    public int getStatus() {
-        return mStatus;
-    }
+    public abstract String getBugReportFileName();
+
+    /**
+     * @return filename of the audio message file stored under the system user.
+     */
+    public abstract String getAudioFileName();
+
+    /**
+     * @return {@link Status} of the bug upload.
+     */
+    public abstract int getStatus();
 
     /**
      * @return StatusMessage of the bug upload.
      */
-    public String getStatusMessage() {
-        return mStatusMessage == null ? "" : mStatusMessage;
-    }
+    public abstract String getStatusMessage();
+
+    /**
+     * @return {@link BugReportType}.
+     */
+    public abstract int getType();
+
+    /** @return {@link Builder} from the meta bug report. */
+    public abstract Builder toBuilder();
 
     @Override
     public int describeContents() {
@@ -94,13 +112,33 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(mId);
-        dest.writeString(mTimestamp);
-        dest.writeString(mTitle);
-        dest.writeString(mUsername);
-        dest.writeString(mFilePath);
-        dest.writeInt(mStatus);
-        dest.writeString(mStatusMessage);
+        dest.writeInt(getId());
+        dest.writeString(getTimestamp());
+        dest.writeString(getTitle());
+        dest.writeString(getUserName());
+        dest.writeString(getFilePath());
+        dest.writeString(getBugReportFileName());
+        dest.writeString(getAudioFileName());
+        dest.writeInt(getStatus());
+        dest.writeString(getStatusMessage());
+        dest.writeInt(getType());
+    }
+
+    /** Converts {@link Date} to bugreport timestamp. */
+    static String toBugReportTimestamp(Date date) {
+        return BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(date);
+    }
+
+    /** Creates a {@link Builder} with default, non-null values. */
+    static Builder builder() {
+        return new AutoValue_MetaBugReport.Builder()
+                .setTimestamp("")
+                .setFilePath("")
+                .setBugReportFileName("")
+                .setAudioFileName("")
+                .setStatusMessage("")
+                .setTitle("")
+                .setUserName("");
     }
 
     /** A creator that's used by Parcelable. */
@@ -112,14 +150,22 @@
                     String title = in.readString();
                     String username = in.readString();
                     String filePath = in.readString();
+                    String bugReportFileName = in.readString();
+                    String audioFileName = in.readString();
                     int status = in.readInt();
                     String statusMessage = in.readString();
-                    return new Builder(id, timestamp)
+                    int type = in.readInt();
+                    return MetaBugReport.builder()
+                            .setId(id)
+                            .setTimestamp(timestamp)
                             .setTitle(title)
                             .setUserName(username)
-                            .setFilepath(filePath)
+                            .setFilePath(filePath)
+                            .setBugReportFileName(bugReportFileName)
+                            .setAudioFileName(audioFileName)
                             .setStatus(status)
                             .setStatusMessage(statusMessage)
+                            .setType(type)
                             .build();
                 }
 
@@ -129,59 +175,38 @@
             };
 
     /** Builder for MetaBugReport. */
-    public static class Builder {
-        private final int mId;
-        private final String mTimestamp;
-        private String mTitle;
-        private String mUsername;
-        private String mFilePath;
-        private int mStatus;
-        private String mStatusMessage;
+    @AutoValue.Builder
+    abstract static class Builder {
+        /** Sets id. */
+        public abstract Builder setId(int id);
 
-        /**
-         * Initializes MetaBugReport.Builder.
-         *
-         * @param id        - mandatory bugreport id
-         * @param timestamp - mandatory timestamp when bugreport initialized.
-         */
-        public Builder(int id, String timestamp) {
-            mId = id;
-            mTimestamp = timestamp;
-        }
+        /** Sets timestamp. */
+        public abstract Builder setTimestamp(String timestamp);
 
         /** Sets title. */
-        public Builder setTitle(String title) {
-            mTitle = title;
-            return this;
-        }
+        public abstract Builder setTitle(String title);
 
         /** Sets username. */
-        public Builder setUserName(String username) {
-            mUsername = username;
-            return this;
-        }
+        public abstract Builder setUserName(String username);
 
         /** Sets filepath. */
-        public Builder setFilepath(String filePath) {
-            mFilePath = filePath;
-            return this;
-        }
+        public abstract Builder setFilePath(String filePath);
 
-        /** Sets status. */
-        public Builder setStatus(int status) {
-            mStatus = status;
-            return this;
-        }
+        /** Sets bugReportFileName. */
+        public abstract Builder setBugReportFileName(String bugReportFileName);
+
+        /** Sets audioFileName. */
+        public abstract Builder setAudioFileName(String audioFileName);
+
+        /** Sets {@link Status}. */
+        public abstract Builder setStatus(int status);
 
         /** Sets statusmessage. */
-        public Builder setStatusMessage(String statusMessage) {
-            mStatusMessage = statusMessage;
-            return this;
-        }
+        public abstract Builder setStatusMessage(String statusMessage);
 
-        /** Returns a {@link MetaBugReport}. */
-        public MetaBugReport build() {
-            return new MetaBugReport(this);
-        }
+        /** Sets the {@link BugReportType}. */
+        public abstract Builder setType(@BugReportType int type);
+
+        public abstract MetaBugReport build();
     }
 }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
index 779750c..32dc804 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
@@ -29,18 +29,24 @@
 import com.google.api.client.json.jackson2.JacksonFactory;
 import com.google.api.services.storage.Storage;
 import com.google.api.services.storage.model.StorageObject;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 
+import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.zip.ZipOutputStream;
 
 /**
- * Uploads a file to GCS using a simple (no-multipart / no-resume) upload policy.
+ * Uploads a bugreport files to GCS using a simple (no-multipart / no-resume) upload policy.
+ *
+ * <p>It merges bugreport zip file and audio file into one final zip file and uploads it.
  *
  * <p>Please see {@code res/values/configs.xml} and {@code res/raw/gcs_credentials.json} for the
  * configuration.
@@ -113,14 +119,51 @@
         Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential)
                 .setApplicationName("Bugreportupload/1.0").build();
 
-        File bugReportFile = new File(bugReport.getFilePath());
-        String fileName = bugReportFile.getName();
-        try (FileInputStream inputStream = new FileInputStream(bugReportFile)) {
-            StorageObject object = uploadSimple(storage, bugReport, fileName, inputStream);
-            Log.v(TAG, "finished uploading object " + object.getName() + " file " + fileName);
+        File tmpBugReportFile = zipBugReportFiles(bugReport);
+        Log.d(TAG, "Uploading file " + tmpBugReportFile);
+        try {
+            // Upload filename is bugreport filename, although, now it contains the audio message.
+            String fileName = bugReport.getBugReportFileName();
+            try (FileInputStream inputStream = new FileInputStream(tmpBugReportFile)) {
+                StorageObject object = uploadSimple(storage, bugReport, fileName, inputStream);
+                Log.v(TAG, "finished uploading object " + object.getName() + " file " + fileName);
+            }
+            File pendingDir = FileUtils.getPendingDir(mContext);
+            // Delete only after successful upload; the files are needed for retry.
+            if (!Strings.isNullOrEmpty(bugReport.getAudioFileName())) {
+                Log.v(TAG, "Deleting file " + bugReport.getAudioFileName());
+                new File(pendingDir, bugReport.getAudioFileName()).delete();
+            }
+            if (!Strings.isNullOrEmpty(bugReport.getBugReportFileName())) {
+                Log.v(TAG, "Deleting file " + bugReport.getBugReportFileName());
+                new File(pendingDir, bugReport.getBugReportFileName()).delete();
+            }
+        } finally {
+            // Delete the temp file if it's not a MetaBugReport#getFilePath, because it's needed
+            // for retry.
+            if (Strings.isNullOrEmpty(bugReport.getFilePath())) {
+                Log.v(TAG, "Deleting file " + tmpBugReportFile);
+                tmpBugReportFile.delete();
+            }
         }
-        Log.v(TAG, "Deleting file " + fileName);
-        bugReportFile.delete();
+    }
+
+    private File zipBugReportFiles(MetaBugReport bugReport) throws IOException {
+        if (!Strings.isNullOrEmpty(bugReport.getFilePath())) {
+            // Old bugreports still have this field.
+            return new File(bugReport.getFilePath());
+        }
+        File finalZipFile =
+                File.createTempFile("bugreport", ".zip", mContext.getCacheDir());
+        File pendingDir = FileUtils.getPendingDir(mContext);
+        try (ZipOutputStream zipStream = new ZipOutputStream(
+                new BufferedOutputStream(new FileOutputStream(finalZipFile)))) {
+            ZipUtils.extractZippedFileToZipStream(
+                    new File(pendingDir, bugReport.getBugReportFileName()), zipStream);
+            ZipUtils.addFileToZipStream(
+                    new File(pendingDir, bugReport.getAudioFileName()), zipStream);
+        }
+        return finalZipFile;
     }
 
     @Override
@@ -131,7 +174,7 @@
     /** Returns true is there are more files to upload. */
     @Override
     protected Boolean doInBackground(Void... voids) {
-        List<MetaBugReport> bugReports = BugStorageUtils.getPendingBugReports(mContext);
+        List<MetaBugReport> bugReports = BugStorageUtils.getUploadPendingBugReports(mContext);
 
         for (MetaBugReport bugReport : bugReports) {
             try {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java b/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
index abe729a..7e89d2d 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
@@ -33,6 +33,9 @@
 
     @Override
     public void onReceive(Context context, Intent intent) {
+        if (!Config.isBugReportEnabled()) {
+            return;
+        }
         // Run it only once for the system user (u0) and ignore for other users.
         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
         if (!userManager.isSystemUser()) {
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..380944e 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,16 @@
     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),
+
+    // Bugreport is expired. Associated file is deleted from the disk.
+    STATUS_EXPIRED(10),
+
+    // Bugreport needs audio message.
+    STATUS_AUDIO_PENDING(11);
 
     private final int mValue;
 
@@ -76,6 +85,12 @@
                 return "Move successful";
             case 8:
                 return "Move failed";
+            case 9:
+                return "Move in progress";
+            case 10:
+                return "Expired";
+            case 11:
+                return "Audio message pending";
         }
         return "unknown";
     }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java b/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
index 67cc560..b2c17e9 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
@@ -27,6 +27,9 @@
 
     @Override
     public boolean onStartJob(final JobParameters jobParameters) {
+        if (!Config.isBugReportEnabled()) {
+            return false;
+        }
         Log.v(TAG, "Starting upload job");
         mUploader = new SimpleUploaderAsyncTask(
                 this, reschedule -> jobFinished(jobParameters, reschedule));
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java
new file mode 100644
index 0000000..e33b706
--- /dev/null
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.bugreport;
+
+import android.util.Log;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/** Zip utility functions. */
+final class ZipUtils {
+    private static final String TAG = ZipUtils.class.getSimpleName();
+
+    /** Extracts the contents of a zip file to the zip output stream. */
+    static void extractZippedFileToZipStream(File file, ZipOutputStream zipStream) {
+        if (!file.exists()) {
+            Log.w(TAG, "File " + file + " not found");
+            return;
+        }
+        if (file.length() == 0) {
+            // If there were issues with reading from dumpstate socket, the dumpstate zip
+            // file still might be available in
+            // /data/user_de/0/com.android.shell/files/bugreports/.
+            Log.w(TAG, "Zip file " + file.getName() + " is empty, skipping.");
+            return;
+        }
+        try (ZipFile zipFile = new ZipFile(file)) {
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
+                try (InputStream stream = zipFile.getInputStream(entry)) {
+                    writeInputStreamToZipStream(entry.getName(), stream, zipStream);
+                }
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to add " + file + " to zip", e);
+        }
+    }
+
+    /** Adds a file to the zip output stream. */
+    static void addFileToZipStream(File file, ZipOutputStream zipStream) {
+        if (!file.exists()) {
+            Log.w(TAG, "File " + file + " not found");
+            return;
+        }
+        if (file.length() == 0) {
+            Log.w(TAG, "File " + file.getName() + " is empty, skipping.");
+            return;
+        }
+        try (FileInputStream audioInput = new FileInputStream(file)) {
+            writeInputStreamToZipStream(file.getName(), audioInput, zipStream);
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to add " + file + "to the final zip");
+        }
+    }
+
+    private static void writeInputStreamToZipStream(
+            String filename, InputStream input, ZipOutputStream zipStream) throws IOException {
+        ZipEntry entry = new ZipEntry(filename);
+        zipStream.putNextEntry(entry);
+        ByteStreams.copy(input, zipStream);
+        zipStream.closeEntry();
+    }
+
+    private ZipUtils() {}
+}
diff --git a/tests/BugReportApp/tests/Android.mk b/tests/BugReportApp/tests/Android.mk
new file mode 100644
index 0000000..2a6ab88
--- /dev/null
+++ b/tests/BugReportApp/tests/Android.mk
@@ -0,0 +1,40 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BugReportAppTest
+LOCAL_INSTRUMENTATION_FOR := BugReportApp
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_JAVA_LIBRARIES := \
+    android.test.base \
+    android.test.mock \
+    android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    truth-prebuilt
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
diff --git a/tests/BugReportApp/tests/AndroidManifest.xml b/tests/BugReportApp/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e6a8537
--- /dev/null
+++ b/tests/BugReportApp/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.car.bugreport.tests" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:label="BugReportAppTest"
+            android:targetPackage="com.google.android.car.bugreport" />
+</manifest>
diff --git a/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
new file mode 100644
index 0000000..747cac4
--- /dev/null
+++ b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.bugreport;
+
+import static com.google.android.car.bugreport.MetaBugReport.TYPE_INTERACTIVE;
+import static com.google.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.Date;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class BugStorageUtilsTest {
+    private static final String TIMESTAMP_TODAY = MetaBugReport.toBugReportTimestamp(new Date());
+    private static final String BUGREPORT_ZIP_FILE_NAME = "bugreport@ASD.zip";
+    private static final int BUGREPORT_ZIP_FILE_CONTENT = 1;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    @Test
+    public void test_createBugReport_createsAndReturnsMetaBugReport() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get()).isEqualTo(bug);
+    }
+
+    @Test
+    public void test_expireBugReport_marksBugReportDeletedAndDeletesZip() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+        try (InputStream in = BugStorageUtils.openBugReportFileToRead(mContext, bug)) {
+            assertThat(in).isNotNull();
+        }
+        Instant now = Instant.now();
+
+        boolean deleteResult = BugStorageUtils.expireBugReport(mContext, bug, now);
+
+        assertThat(deleteResult).isTrue();
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get())
+                .isEqualTo(bug.toBuilder()
+                        .setStatus(Status.STATUS_EXPIRED.getValue())
+                        .setStatusMessage("Expired on " + now).build());
+        assertThrows(FileNotFoundException.class, () ->
+                BugStorageUtils.openBugReportFileToRead(mContext, bug));
+    }
+
+    @Test
+    public void test_completeDeleteBugReport_removesBugReportRecordFromDb() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+        try (InputStream in = BugStorageUtils.openBugReportFileToRead(mContext, bug)) {
+            assertThat(in).isNotNull();
+        }
+
+        boolean deleteResult = BugStorageUtils.completeDeleteBugReport(mContext, bug.getId());
+
+        assertThat(deleteResult).isTrue();
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).isPresent()).isFalse();
+        assertThrows(IllegalArgumentException.class, () ->
+                BugStorageUtils.openBugReportFileToRead(mContext, bug));
+    }
+
+    private MetaBugReport createBugReportWithStatus(
+            String timestamp, Status status, int type, boolean createBugReportFile)
+            throws IOException {
+        MetaBugReport bugReport = BugStorageUtils.createBugReport(
+                mContext, "sample title", timestamp, "driver", type);
+        if (createBugReportFile) {
+            bugReport = BugStorageUtils.update(mContext,
+                    bugReport.toBuilder().setBugReportFileName(BUGREPORT_ZIP_FILE_NAME).build());
+            try (OutputStream out = BugStorageUtils.openBugReportFileToWrite(mContext, bugReport)) {
+                out.write(BUGREPORT_ZIP_FILE_CONTENT);
+            }
+        }
+        return BugStorageUtils.setBugReportStatus(mContext, bugReport, status, "");
+    }
+
+    private static void assertThrows(Class<? extends Throwable> exceptionClass,
+            ExceptionRunnable r) {
+        try {
+            r.run();
+        } catch (Throwable e) {
+            assertTrue("Expected exception type " + exceptionClass.getName() + " but got "
+                    + e.getClass().getName(), exceptionClass.isAssignableFrom(e.getClass()));
+            return;
+        }
+        fail("Expected exception type " + exceptionClass.getName()
+                + ", but no exception was thrown");
+    }
+
+    private interface ExceptionRunnable {
+        void run() throws Exception;
+    }
+}
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/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
index 6309b42..6c32f5c 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
@@ -310,7 +310,7 @@
         mHandler.sendEmptyMessageDelayed(MESSAGE_STOP_SCAN_WIFI_LIST, DELAY_TIME_STOP_SCAN_MS);
 
         if (mFilterWifiTracker == null) {
-            mFilterWifiTracker = new FilterWifiTracker(getActivity(), getSettingsLifecycle());
+            mFilterWifiTracker = new FilterWifiTracker(getContext(), getSettingsLifecycle());
         }
         mFilterWifiTracker.onResume();
     }
@@ -473,11 +473,13 @@
     private final class FilterWifiTracker {
         private final List<String> mAccessPointKeys;
         private final WifiTracker mWifiTracker;
+        private final Context mContext;
 
         public FilterWifiTracker(Context context, Lifecycle lifecycle) {
             mWifiTracker = WifiTrackerFactory.create(context, mWifiListener,
                     lifecycle, /* includeSaved */ true, /* includeScans */ true);
             mAccessPointKeys = new ArrayList<>();
+            mContext = context;
         }
 
         /**
@@ -486,7 +488,7 @@
          */
         public void updateKeys(List<ScanResult> scanResults) {
             for (ScanResult scanResult : scanResults) {
-                final String key = AccessPoint.getKey(scanResult);
+                final String key = AccessPoint.getKey(mContext, scanResult);
                 if (!mAccessPointKeys.contains(key)) {
                     mAccessPointKeys.add(key);
                 }
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/CarVoiceServiceTriggerApp/Android.mk b/tests/CarVoiceServiceTriggerApp/Android.mk
new file mode 100644
index 0000000..b492e1d
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/Android.mk
@@ -0,0 +1,50 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+ifneq ($(TARGET_BUILD_PDK),true)
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := CarVoiceTriggerApp
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+
+include $(BUILD_MULTI_PREBUILT)
+
+include $(CLEAR_VARS)
+
+endif #TARGET_BUILD_PDK
diff --git a/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml b/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml
new file mode 100644
index 0000000..0e43fa7
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.voicetrigger">
+
+    <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE"/>
+
+    <application android:label="@string/app_title">
+        <receiver android:name=".VoiceTriggerReceiver" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.VOICE_ASSIST"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
+
diff --git a/tests/CarVoiceServiceTriggerApp/res/values/strings.xml b/tests/CarVoiceServiceTriggerApp/res/values/strings.xml
new file mode 100644
index 0000000..3edaf54
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_title" translatable="false">Voice Service Trigger</string>
+</resources>
diff --git a/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java b/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java
new file mode 100644
index 0000000..f022fb1
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java
@@ -0,0 +1,42 @@
+/*
+ * 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.voicetrigger;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.service.voice.VoiceInteractionSession;
+
+import com.android.internal.app.AssistUtils;
+
+/**
+ * The exported {@link BroadcastReceiver} which receives an Intent to trigger the current active
+ * voice service. The voice service will be triggered as if the assistant button in the system UI
+ * is clicked.
+ *
+ * Run adb shell am broadcast -a android.intent.action.VOICE_ASSIST -n
+ * com.android.voicetrigger/.VoiceTriggerReceiver to use.
+ */
+public class VoiceTriggerReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        AssistUtils assistUtils = new AssistUtils(context);
+        assistUtils.showSessionForActiveService(new Bundle(),
+                VoiceInteractionSession.SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI, null, null);
+    }
+}
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..056fe9c 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -142,6 +142,86 @@
         mSystemStateInterface.waitForShutdown(WAIT_TIMEOUT_MS);
     }
 
+    public void testSuspend() throws Exception {
+        final int wakeupTime = 100;
+        initTest(wakeupTime);
+
+        // Start in the ON state
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+        assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+        // Request suspend
+        mPowerHal.setCurrentPowerState(
+                new PowerState(
+                        VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.CAN_SLEEP));
+        // Verify suspend
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+    }
+
+    public void testShutdownOnSuspend() throws Exception {
+        final int wakeupTime = 100;
+        initTest(wakeupTime);
+
+        // Start in the ON state
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+        assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+        // Tell it to shutdown
+        mService.requestShutdownOnNextSuspend();
+        // Request suspend
+        mPowerHal.setCurrentPowerState(
+                new PowerState(
+                        VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.CAN_SLEEP));
+        // Verify shutdown
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_SHUTDOWN_START, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+        mPowerSignalListener.waitForShutdown(WAIT_TIMEOUT_MS);
+        // Send the finished signal
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.FINISHED, 0));
+        mSystemStateInterface.waitForShutdown(WAIT_TIMEOUT_MS);
+        // Cancel the shutdown
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.CANCEL_SHUTDOWN, 0));
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_SHUTDOWN_CANCELLED, WAIT_TIMEOUT_LONG_MS, 0);
+
+        // Request suspend again
+        mPowerHal.setCurrentPowerState(
+                new PowerState(
+                        VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.CAN_SLEEP));
+        // Verify suspend
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+    }
+
+    public void testShutdownCancel() throws Exception {
+        final int wakeupTime = 100;
+        initTest(wakeupTime);
+
+        // Start in the ON state
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+        assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+        // Start shutting down
+        mPowerHal.setCurrentPowerState(
+                new PowerState(
+                        VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.SHUTDOWN_IMMEDIATELY));
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_SHUTDOWN_START, WAIT_TIMEOUT_LONG_MS, 0);
+        // Cancel the shutdown
+        mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.CANCEL_SHUTDOWN, 0));
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_SHUTDOWN_CANCELLED, WAIT_TIMEOUT_LONG_MS, 0);
+        // Go to suspend
+        mPowerHal.setCurrentPowerState(
+                new PowerState(
+                        VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.CAN_SLEEP));
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+    }
+
     public void testShutdownWithProcessing() throws Exception {
         final int wakeupTime = 100;
         initTest(wakeupTime);
@@ -197,7 +277,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/MockedPowerHalService.java b/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
index 7359a03..770cc85 100644
--- a/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
+++ b/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
@@ -87,6 +87,12 @@
         doSendState(SET_SHUTDOWN_START, wakeupTimeSec);
     }
 
+    @Override
+    public void sendShutdownCancel() {
+        Log.i(TAG, "sendShutdownCancel");
+        doSendState(SET_SHUTDOWN_CANCELLED, 0);
+    }
+
     public synchronized int[] waitForSend(long timeoutMs) throws Exception {
         if (mSentStates.size() == 0) {
             wait(timeoutMs);
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..2bd09df 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.VmsClientLogger;
 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 VmsClientLogger mPublisherLog;
+    @Mock
+    private VmsClientLogger mSubscriberLog;
+    @Mock
+    private VmsClientLogger mSubscriberLog2;
+    @Mock
+    private VmsClientLogger 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.getVmsClientLogger(PUBLISHER_UID)).thenReturn(mPublisherLog);
+        when(mStatsService.getVmsClientLogger(SUBSCRIBER_UID)).thenReturn(mSubscriberLog);
+        when(mStatsService.getVmsClientLogger(SUBSCRIBER_UID2)).thenReturn(mSubscriberLog2);
+        when(mStatsService.getVmsClientLogger(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..9bdcaa6
--- /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.VmsClientLogger.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.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        validateConnectionStats("10101,test.package,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Connected() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        validateConnectionStats("10101,test.package,0,1,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Disconnected() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        validateConnectionStats("10101,test.package,0,0,1,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Terminated() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.TERMINATED);
+        validateConnectionStats("10101,test.package,0,0,0,1,0");
+    }
+
+    @Test
+    public void testLogConnectionState_ConnectionError() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTION_ERROR);
+        validateConnectionStats("10101,test.package,0,0,0,0,1");
+    }
+
+    @Test
+    public void testLogConnectionState_UnknownUID() {
+        mCarStatsService.getVmsClientLogger(-1)
+                .logConnectionState(ConnectionState.CONNECTING);
+        testEmptyStats();
+    }
+
+    @Test
+    public void testLogConnectionState_MultipleClients_MultipleStates() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logConnectionState(ConnectionState.TERMINATED);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 5);
+        validateClientStats("10101,1,2,3,5,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultiplePackets() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,6,3,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultipleLayers() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,5,1,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultiplePackets() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,6,3,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultipleLayers() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER2, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,0,0,5,1");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultiplePackets() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,0,0,6,3");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultipleLayers() {
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER2, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(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.getVmsClientLogger(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLogger(-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..e1c9d9a 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.VmsClientLogger;
+import com.android.car.stats.VmsClientLogger.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 VmsClientLogger mSystemClientLog;
+    @Mock
+    private VmsClientLogger mUserClientLog;
+    @Mock
+    private VmsClientLogger mUserClientLog2;
+    @Mock
+    private VmsClientLogger 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.getVmsClientLogger(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.getVmsClientLogger(TEST_USER_UID)).thenReturn(mUserClientLog);
+        when(mStatsService.getVmsClientLogger(TEST_USER_UID_U11)).thenReturn(mUserClientLog2);
+
+        when(mStatsService.getVmsClientLogger(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,29 @@
     }
 
     private void notifyUserSwitched(int foregroundUserId, boolean isForegroundUserUnlocked) {
-        notifyUserAction(foregroundUserId, isForegroundUserUnlocked, Intent.ACTION_USER_SWITCHED);
+        when(mUserManager.isUserUnlockingOrUnlocked(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));
+        when(mUserManager.isUserUnlockingOrUnlocked(foregroundUserId))
+                .thenReturn(isForegroundUserUnlocked);
+        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 +1070,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;
     }