Merge "Cleanup old internal APIs from carbugreportmanagerservice" into qt-dev
diff --git a/car-lib/api/test-current.txt b/car-lib/api/test-current.txt
index d6b162e..515bd93 100644
--- a/car-lib/api/test-current.txt
+++ b/car-lib/api/test-current.txt
@@ -7,6 +7,14 @@
 
 }
 
+package android.car.drivingstate {
+
+  public final class CarDrivingStateManager {
+    method public void injectDrivingState(int);
+  }
+
+}
+
 package android.car.media {
 
   public final class CarAudioManager {
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
index 896534a..9b0626f 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.car.Car;
 import android.car.CarManagerBase;
 import android.content.Context;
 import android.os.Handler;
@@ -26,15 +28,18 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.Log;
 
 import java.lang.ref.WeakReference;
 
 /**
  * API to register and get driving state related information in a car.
+ *
  * @hide
  */
 @SystemApi
+@TestApi
 public final class CarDrivingStateManager implements CarManagerBase {
     private static final String TAG = "CarDrivingStateMgr";
     private static final boolean DBG = false;
@@ -64,7 +69,10 @@
 
     /**
      * Listener Interface for clients to implement to get updated on driving state changes.
+     *
+     * @hide
      */
+    @SystemApi
     public interface CarDrivingStateEventListener {
         /**
          * Called when the car's driving state changes.
@@ -77,7 +85,10 @@
      * Register a {@link CarDrivingStateEventListener} to listen for driving state changes.
      *
      * @param listener  {@link CarDrivingStateEventListener}
+     *
+     * @hide
      */
+    @SystemApi
     public synchronized void registerListener(@NonNull CarDrivingStateEventListener listener) {
         if (listener == null) {
             if (VDBG) {
@@ -107,7 +118,10 @@
     /**
      * Unregister the registered {@link CarDrivingStateEventListener} for the given driving event
      * type.
+     *
+     * @hide
      */
+    @SystemApi
     public synchronized void unregisterListener() {
         if (mDrvStateEventListener == null) {
             if (DBG) {
@@ -128,8 +142,11 @@
      * Get the current value of the car's driving state.
      *
      * @return {@link CarDrivingStateEvent} corresponding to the given eventType
+     *
+     * @hide
      */
     @Nullable
+    @SystemApi
     public CarDrivingStateEvent getCurrentCarDrivingState() {
         try {
             return mDrivingService.getCurrentDrivingState();
@@ -139,6 +156,27 @@
     }
 
     /**
+     * Notify registered driving state change listener about injected event.
+     *
+     * @param drivingState Value in {@link CarDrivingStateEvent.CarDrivingState}.
+     *
+     * Requires Permission:
+     * {@link Car#PERMISSION_CONTROL_APP_BLOCKING}
+     *
+     * @hide
+     */
+    @TestApi
+    public void injectDrivingState(int drivingState) {
+        CarDrivingStateEvent event = new CarDrivingStateEvent(
+                drivingState, SystemClock.elapsedRealtimeNanos());
+        try {
+            mDrivingService.injectDrivingState(event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Class that implements the listener interface and gets called back from the
      * {@link com.android.car.CarDrivingStateService} across the binder interface.
      */
diff --git a/car-lib/src/android/car/drivingstate/ICarDrivingState.aidl b/car-lib/src/android/car/drivingstate/ICarDrivingState.aidl
index 30f1542..23c5d03 100644
--- a/car-lib/src/android/car/drivingstate/ICarDrivingState.aidl
+++ b/car-lib/src/android/car/drivingstate/ICarDrivingState.aidl
@@ -31,4 +31,5 @@
     void registerDrivingStateChangeListener(in ICarDrivingStateChangeListener listener) = 0;
     void unregisterDrivingStateChangeListener(in ICarDrivingStateChangeListener listener) = 1;
     CarDrivingStateEvent getCurrentDrivingState() = 2;
+    void injectDrivingState(in CarDrivingStateEvent event) = 3;
 }
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 904542d..6db7961 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
@@ -48,6 +48,7 @@
     <!-- Allow smart unlock immediately after boot because the user shouldn't have to enter a pin
          code to unlock their car head unit. -->
     <bool name="config_strongAuthRequiredOnBoot">false</bool>
+    <string name="config_defaultTrustAgent" translatable="false">com.android.car/com.android.car.trust.CarBleTrustAgent</string>
     <!-- Show Navigation Bar -->
     <bool name="config_showNavigationBar">true</bool>
 
@@ -80,5 +81,8 @@
          display, this value should be true. -->
     <bool name="config_perDisplayFocusEnabled">true</bool>
 
+    <!-- True if the device supports split screen as a form of multi-window. -->
+    <bool name="config_supportsSplitScreenMultiWindow">false</bool>
+
     <string name="config_dataUsageSummaryComponent">com.android.car.settings/com.android.car.settings.datausage.DataWarningAndLimitActivity</string>
 </resources>
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 25b2cfe..7d85445 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -527,10 +527,10 @@
                        android:resource="@xml/car_trust_agent"/>
         </service>
         <activity android:name="com.android.car.pm.ActivityBlockingActivity"
+                  android:documentLaunchMode="always"
                   android:excludeFromRecents="true"
-                  android:theme="@android:style/Theme.Translucent.NoTitleBar"
                   android:exported="false"
-                  android:launchMode="singleTask">
+                  android:theme="@android:style/Theme.Translucent.NoTitleBar">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 9d90a7c..4aef4a5 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -202,8 +202,8 @@
 
     <!-- service/characteristics uuid for unlocking a device -->
     <string name="unlock_service_uuid" translatable="false">00003ac5-0000-1000-8000-00805f9b34fb</string>
-    <string name="unlock_escrow_token_uuid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
-    <string name="unlock_handle_uuid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
+    <string name="unlock_client_write_uuid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
+    <string name="unlock_server_write_uuid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
 
     <string name="token_handle_shared_preferences" translatable="false">com.android.car.trust.TOKEN_HANDLE</string>
 
diff --git a/service/res/xml/car_ux_restrictions_map.xml b/service/res/xml/car_ux_restrictions_map.xml
index 385869a..813796b 100644
--- a/service/res/xml/car_ux_restrictions_map.xml
+++ b/service/res/xml/car_ux_restrictions_map.xml
@@ -55,7 +55,6 @@
         <DrivingState car:state="moving" car:minSpeed="5.0">
             <Restrictions car:requiresDistractionOptimization="true" car:uxr="fully_restricted"/>
         </DrivingState>
-
     </RestrictionMapping>
 
     <!-- Configure restriction parameters here-->
@@ -66,4 +65,4 @@
         <ContentRestrictions car:maxCumulativeItems="21" car:maxDepth="3"/>
     </RestrictionParameters>
 
-</UxRestrictions>
\ No newline at end of file
+</UxRestrictions>
diff --git a/service/src/com/android/car/CarDrivingStateService.java b/service/src/com/android/car/CarDrivingStateService.java
index bc0fbc1..48840b7 100644
--- a/service/src/com/android/car/CarDrivingStateService.java
+++ b/service/src/com/android/car/CarDrivingStateService.java
@@ -17,6 +17,7 @@
 package com.android.car;
 
 import android.annotation.Nullable;
+import android.car.Car;
 import android.car.VehicleAreaType;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
@@ -220,6 +221,15 @@
         return mCurrentDrivingState;
     }
 
+    @Override
+    public synchronized void injectDrivingState(CarDrivingStateEvent event) {
+        ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_APP_BLOCKING);
+
+        for (DrivingStateClient client : mDrivingStateClients) {
+            client.dispatchEventToClients(event);
+        }
+    }
+
     /**
      * Class that holds onto client related information - listener interface, process that hosts the
      * binder object etc.
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 43d4a46..cc57856 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -16,6 +16,8 @@
 
 package com.android.car;
 
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListenerWithCompletion;
@@ -52,8 +54,8 @@
  * This service stores the last known location from {@link LocationManager} when a car is parked
  * and restores the location when the car is powered on.
  */
-public class CarLocationService extends BroadcastReceiver implements
-        CarServiceBase, CarPowerStateListenerWithCompletion {
+public class CarLocationService extends BroadcastReceiver implements CarServiceBase,
+        CarPowerStateListenerWithCompletion {
     private static final String TAG = "CarLocationService";
     private static final String FILENAME = "location_cache.json";
     private static final boolean DBG = true;
@@ -73,10 +75,9 @@
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private CarPowerManager mCarPowerManager;
+    private CarDrivingStateService mCarDrivingStateService;
 
-    public CarLocationService(
-            Context context,
-            CarUserManagerHelper carUserManagerHelper) {
+    public CarLocationService(Context context, CarUserManagerHelper carUserManagerHelper) {
         logd("constructed");
         mContext = context;
         mCarUserManagerHelper = carUserManagerHelper;
@@ -90,6 +91,16 @@
         filter.addAction(LocationManager.MODE_CHANGED_ACTION);
         filter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION);
         mContext.registerReceiver(this, filter);
+        mCarDrivingStateService = CarLocalServices.getService(CarDrivingStateService.class);
+        if (mCarDrivingStateService != null) {
+            CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
+            if (event != null && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+                deleteCacheFile();
+            } else {
+                mCarDrivingStateService.registerDrivingStateChangeListener(
+                        mICarDrivingStateChangeEventListener);
+            }
+        }
         mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
         if (mCarPowerManager != null) { // null case happens for testing.
             mCarPowerManager.setListenerWithCompletion(CarLocationService.this);
@@ -102,6 +113,10 @@
         if (mCarPowerManager != null) {
             mCarPowerManager.clearListener();
         }
+        if (mCarDrivingStateService != null) {
+            mCarDrivingStateService.unregisterDrivingStateChangeListener(
+                    mICarDrivingStateChangeEventListener);
+        }
         mContext.unregisterReceiver(this);
     }
 
@@ -173,6 +188,22 @@
         }
     }
 
+    private final ICarDrivingStateChangeListener mICarDrivingStateChangeEventListener =
+            new ICarDrivingStateChangeListener.Stub() {
+                @Override
+                public void onDrivingStateChanged(CarDrivingStateEvent event) {
+                    logd("onDrivingStateChanged " + event);
+                    if (event != null
+                            && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+                        deleteCacheFile();
+                        if (mCarDrivingStateService != null) {
+                            mCarDrivingStateService.unregisterDrivingStateChangeListener(
+                                    mICarDrivingStateChangeEventListener);
+                        }
+                    }
+                }
+            };
+
     /**
      * Tells whether or not we should check location permissions for the sake of deleting the
      * location cache file when permissions are lacking.  If the system user is headless but the
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 244ee8b..31627b9 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -150,7 +150,7 @@
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
                 serviceContext, mCarAudioService, this);
-        mVmsBrokerService = new VmsBrokerService();
+        mVmsBrokerService = new VmsBrokerService(mContext.getPackageManager());
         mVmsClientManager = new VmsClientManager(
                 serviceContext, mCarUserService, mUserManagerHelper, mHal.getVmsHal());
         mVmsSubscriberService = new VmsSubscriberService(
@@ -169,9 +169,9 @@
 
         CarLocalServices.addService(CarPowerManagementService.class, mCarPowerManagementService);
         CarLocalServices.addService(CarUserService.class, mCarUserService);
-        CarLocalServices.addService(CarTrustedDeviceService.class,
-                mCarTrustedDeviceService);
+        CarLocalServices.addService(CarTrustedDeviceService.class, mCarTrustedDeviceService);
         CarLocalServices.addService(SystemInterface.class, mSystemInterface);
+        CarLocalServices.addService(CarDrivingStateService.class, mCarDrivingStateService);
 
         // Be careful with order. Service depending on other service should be inited later.
         List<CarServiceBase> allServices = new ArrayList<>();
diff --git a/service/src/com/android/car/SystemActivityMonitoringService.java b/service/src/com/android/car/SystemActivityMonitoringService.java
index 3894337..d192727 100644
--- a/service/src/com/android/car/SystemActivityMonitoringService.java
+++ b/service/src/com/android/car/SystemActivityMonitoringService.java
@@ -15,6 +15,8 @@
  */
 package com.android.car;
 
+import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_DISPLAY_ID;
+
 import android.app.ActivityManager;
 import android.app.ActivityManager.StackInfo;
 import android.app.ActivityOptions;
@@ -104,7 +106,7 @@
     private final HandlerThread mMonitorHandlerThread;
     private final ActivityMonitorHandler mHandler;
 
-    /** K: stack id, V: top task */
+    /** K: display id, V: top task */
     private final SparseArray<TopTaskInfoContainer> mTopTasks = new SparseArray<>();
     /** K: uid, V : list of pid */
     private final Map<Integer, Set<Integer>> mForegroundUidPids = new ArrayMap<>();
@@ -377,10 +379,14 @@
      * block the current task with the provided new activity.
      */
     private void handleBlockActivity(TopTaskInfoContainer currentTask, Intent newActivityIntent) {
-        // Only block default display.
-        ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
+        int displayId = newActivityIntent.getIntExtra(BLOCKING_INTENT_EXTRA_DISPLAY_ID,
+                Display.DEFAULT_DISPLAY);
+        if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
+            Log.d(CarLog.TAG_AM, "Launching blocking activity on display: " + displayId);
+        }
 
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(displayId);
         mContext.startActivityAsUser(newActivityIntent, options.toBundle(),
                 new UserHandle(currentTask.stackInfo.userId));
         // Now make stack with new activity focused.
diff --git a/service/src/com/android/car/Utils.java b/service/src/com/android/car/Utils.java
index 1b7c029..2b468b7 100644
--- a/service/src/com/android/car/Utils.java
+++ b/service/src/com/android/car/Utils.java
@@ -15,15 +15,16 @@
  */
 package com.android.car;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.util.SparseArray;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.UUID;
@@ -253,4 +254,28 @@
                 ThreadLocalRandom.current().nextInt((int) Math.pow(10, length)));
     }
 
+
+    /**
+     * Concatentate the given 2 byte arrays
+     *
+     * @param a input array 1
+     * @param b input array 2
+     * @return concatenated array of arrays 1 and 2
+     */
+    @Nullable
+    public static byte[] concatByteArrays(@Nullable byte[] a, @Nullable byte[] b) {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            if (a != null) {
+                outputStream.write(a);
+            }
+            if (b != null) {
+                outputStream.write(b);
+            }
+        } catch (IOException e) {
+            return null;
+        }
+        return outputStream.toByteArray();
+    }
+
 }
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 0c6ff64..3384e3a 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -31,12 +31,16 @@
 
 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;
 
+
 /**
  * Receives HAL updates by implementing VmsHalService.VmsHalListener.
  * Binds to publishers and configures them to use this service.
@@ -46,12 +50,68 @@
     private static final boolean DBG = true;
     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 VmsClientManager mClientManager;
     private final VmsBrokerService mBrokerService;
     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(
             Context context,
             VmsBrokerService brokerService,
@@ -76,7 +136,29 @@
     @Override
     public void dump(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
-        writer.println("mPublisherProxies:" + mPublisherProxies.keySet());
+        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);
+            }
+        }
     }
 
     @Override
@@ -143,6 +225,26 @@
             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);
@@ -150,16 +252,32 @@
                 Log.d(TAG, String.format("Publishing to %s as %d (%s)", layer, publisherId, mName));
             }
 
+            if (layer == null) {
+                return;
+            }
+
+            int payloadLength = payload != null ? payload.length : 0;
+            incrementPacketCount(layer, payloadLength);
+
             // Send the message to subscribers
             Set<IVmsSubscriberClient> listeners =
                     mBrokerService.getSubscribersForLayerFromPublisher(layer, publisherId);
 
             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);
+            }
+
             for (IVmsSubscriberClient listener : listeners) {
                 try {
                     listener.onVmsMessageReceived(layer, payload);
                 } catch (RemoteException ex) {
-                    Log.e(TAG, String.format("Unable to publish to listener: %s", listener));
+                    String subscriberName = mBrokerService.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/hal/VmsHalService.java b/service/src/com/android/car/hal/VmsHalService.java
index 487ec1f..3f39f48 100644
--- a/service/src/com/android/car/hal/VmsHalService.java
+++ b/service/src/com/android/car/hal/VmsHalService.java
@@ -283,15 +283,15 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        writer.println(TAG);
-        writer.println("VmsProperty " + (mIsSupported ? "" : "not") + " supported.");
+        writer.println("*VMS HAL*");
 
-        writer.println(
-                "VmsPublisherService " + (mPublisherService != null ? "" : "not") + " registered.");
+        writer.println("VmsProperty: " + (mIsSupported ? "supported" : "unsupported"));
+        writer.println("VmsPublisherService: "
+                + (mPublisherService != null ? "registered " : "unregistered"));
         writer.println("mSubscriptionStateSequence: " + mSubscriptionStateSequence);
 
-        writer.println("VmsSubscriberService " + (mSubscriberService != null ? "" : "not")
-                + " registered.");
+        writer.println("VmsSubscriberService: "
+                + (mSubscriberService != null ? "registered" : "unregistered"));
         writer.println("mAvailableLayersSequence: " + mAvailableLayersSequence);
     }
 
diff --git a/service/src/com/android/car/pm/CarPackageManagerService.java b/service/src/com/android/car/pm/CarPackageManagerService.java
index 7615d28..0f33ff4 100644
--- a/service/src/com/android/car/pm/CarPackageManagerService.java
+++ b/service/src/com/android/car/pm/CarPackageManagerService.java
@@ -42,6 +42,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.Signature;
 import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -54,7 +55,9 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.SparseArray;
 import android.view.Display;
+import android.view.DisplayAddress;
 
 import com.android.car.CarLog;
 import com.android.car.CarServiceBase;
@@ -89,6 +92,7 @@
     private final SystemActivityMonitoringService mSystemActivityMonitoringService;
     private final PackageManager mPackageManager;
     private final ActivityManager mActivityManager;
+    private final DisplayManager mDisplayManager;
 
     private final HandlerThread mHandlerThread;
     private final PackageHandler mHandler;
@@ -110,7 +114,6 @@
     private final HashMap<String, ClientPolicy> mClientPolicies = new HashMap<>();
     @GuardedBy("this")
     private HashMap<String, AppBlockingPackageInfoWrapper> mActivityWhitelistMap = new HashMap<>();
-    // The list corresponding to the one configured in <activityBlacklist>
     @GuardedBy("this")
     private LinkedList<AppBlockingPolicyProxy> mProxies;
 
@@ -122,7 +125,10 @@
     private final ComponentName mActivityBlockingActivity;
 
     private final ActivityLaunchListener mActivityLaunchListener = new ActivityLaunchListener();
-    private final UxRestrictionsListener mUxRestrictionsListener;
+    // K: (logical) display id of a physical display, V: UXR change listener of this display.
+    // For multi-display, monitor UXR change on each display.
+    private final SparseArray<UxRestrictionsListener> mUxRestrictionsListeners =
+            new SparseArray<>();
     private final VendorServiceController mVendorServiceController;
 
     // Information related to when the installed packages should be parsed for building a white and
@@ -171,6 +177,12 @@
      */
     public static final String BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO = "is_root_activity_do";
 
+    /**
+     * int display id of the blocked task.
+     * @hide
+     */
+    public static final String BLOCKING_INTENT_EXTRA_DISPLAY_ID = "display_id";
+
     public CarPackageManagerService(Context context,
             CarUxRestrictionsManagerService uxRestrictionsService,
             SystemActivityMonitoringService systemActivityMonitoringService,
@@ -180,7 +192,7 @@
         mSystemActivityMonitoringService = systemActivityMonitoringService;
         mPackageManager = mContext.getPackageManager();
         mActivityManager = mContext.getSystemService(ActivityManager.class);
-        mUxRestrictionsListener = new UxRestrictionsListener(uxRestrictionsService);
+        mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mHandlerThread = new HandlerThread(CarLog.TAG_PACKAGE);
         mHandlerThread.start();
         mHandler = new PackageHandler(mHandlerThread.getLooper());
@@ -281,14 +293,14 @@
 
     @Override
     public boolean isActivityBackedBySafeActivity(ComponentName activityName) {
-        if (!mUxRestrictionsListener.isRestricted()) {
-            return true;
-        }
         StackInfo info = mSystemActivityMonitoringService.getFocusedStackForTopActivity(
                 activityName);
         if (info == null) { // not top in focused stack
             return true;
         }
+        if (!isUxRestrictedOnDisplay(info.displayId)) {
+            return true;
+        }
         if (info.taskNames.length <= 1) { // nothing below this.
             return false;
         }
@@ -385,8 +397,11 @@
         }
         mContext.unregisterReceiver(mPackageParsingEventReceiver);
         mContext.unregisterReceiver(mUserSwitchedEventReceiver);
-        mCarUxRestrictionsService.unregisterUxRestrictionsChangeListener(mUxRestrictionsListener);
         mSystemActivityMonitoringService.registerActivityLaunchListener(null);
+        for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
+            UxRestrictionsListener listener = mUxRestrictionsListeners.valueAt(i);
+            mCarUxRestrictionsService.unregisterUxRestrictionsChangeListener(listener);
+        }
     }
 
     // run from HandlerThread
@@ -402,15 +417,22 @@
         pkgParseIntent.addDataScheme("package");
         mContext.registerReceiverAsUser(mPackageParsingEventReceiver, UserHandle.ALL,
                 pkgParseIntent, null, null);
-        try {
-            // TODO(128456985): register listener for each display in order to
-            // properly launch blocking screens.
-            mCarUxRestrictionsService.registerUxRestrictionsChangeListener(
-                    mUxRestrictionsListener, Display.DEFAULT_DISPLAY);
-        } catch (IllegalArgumentException e) {
-            // can happen while mocking is going on while init is still done.
-            Log.w(CarLog.TAG_PACKAGE, "sensor subscription failed", e);
-            return;
+
+        List<Display> physicalDisplays = getPhysicalDisplays();
+
+        // Assume default display (display 0) is always a physical display.
+        Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+        if (!physicalDisplays.contains(defaultDisplay)) {
+            if (Log.isLoggable(CarLog.TAG_PACKAGE, Log.INFO)) {
+                Log.i(CarLog.TAG_PACKAGE, "Adding default display to physical displays.");
+            }
+            physicalDisplays.add(defaultDisplay);
+        }
+        for (Display physicalDisplay : physicalDisplays) {
+            int displayId = physicalDisplay.getDisplayId();
+            UxRestrictionsListener listener = new UxRestrictionsListener(mCarUxRestrictionsService);
+            mUxRestrictionsListeners.put(displayId, listener);
+            mCarUxRestrictionsService.registerUxRestrictionsChangeListener(listener, displayId);
         }
         mSystemActivityMonitoringService.registerActivityLaunchListener(
                 mActivityLaunchListener);
@@ -423,7 +445,7 @@
         synchronized (this) {
             mHasParsedPackages = true;
         }
-        mUxRestrictionsListener.checkIfTopActivityNeedsBlocking();
+        blockTopActivitiesIfNecessary();
     }
 
     private synchronized void doHandleRelease() {
@@ -901,7 +923,14 @@
             writer.println("*PackageManagementService*");
             writer.println("mEnableActivityBlocking:" + mEnableActivityBlocking);
             writer.println("mHasParsedPackages:" + mHasParsedPackages);
-            writer.println("ActivityRestricted:" + mUxRestrictionsListener.isRestricted());
+            List<String> restrictions = new ArrayList<>(mUxRestrictionsListeners.size());
+            for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
+                int displayId = mUxRestrictionsListeners.keyAt(i);
+                UxRestrictionsListener listener = mUxRestrictionsListeners.valueAt(i);
+                restrictions.add(String.format("Display %d is %s",
+                        displayId, (listener.isRestricted() ? "restricted" : "unrestricted")));
+            }
+            writer.println("Display Restrictions:\n" + String.join("\n", restrictions));
             writer.println(String.join("\n", mBlockedActivityLogs));
             writer.print(dumpPoliciesLocked(true));
         }
@@ -946,15 +975,55 @@
         return sb.toString();
     }
 
+    /**
+     * Returns display with physical address.
+     */
+    private List<Display> getPhysicalDisplays() {
+        List<Display> displays = new ArrayList<>();
+        for (Display display : mDisplayManager.getDisplays()) {
+            if (display.getAddress() instanceof DisplayAddress.Physical) {
+                displays.add(display);
+            }
+        }
+        return displays;
+    }
+
+    /**
+     * Returns whether UX restrictions is required for display.
+     *
+     * Non-physical display will use restrictions for {@link Display#DEFAULT_DISPLAY}.
+     */
+    private boolean isUxRestrictedOnDisplay(int displayId) {
+        UxRestrictionsListener listenerForTopTaskDisplay;
+        if (mUxRestrictionsListeners.indexOfKey(displayId) < 0) {
+            listenerForTopTaskDisplay = mUxRestrictionsListeners.get(Display.DEFAULT_DISPLAY);
+            if (listenerForTopTaskDisplay == null) {
+                // This should never happen.
+                Log.e(CarLog.TAG_PACKAGE, "Missing listener for default display.");
+                return true;
+            }
+        } else {
+            listenerForTopTaskDisplay = mUxRestrictionsListeners.get(displayId);
+        }
+
+        return listenerForTopTaskDisplay.isRestricted();
+    }
+
+    private void blockTopActivitiesIfNecessary() {
+        List<TopTaskInfoContainer> topTasks = mSystemActivityMonitoringService.getTopTasks();
+        for (TopTaskInfoContainer topTask : topTasks) {
+            if (topTask == null) {
+                Log.e(CarLog.TAG_PACKAGE, "Top tasks contains null.");
+                continue;
+            }
+            blockTopActivityIfNecessary(topTask);
+        }
+    }
+
     private void blockTopActivityIfNecessary(TopTaskInfoContainer topTask) {
-        // Only block activities launched on default display.
-        if (topTask.displayId != Display.DEFAULT_DISPLAY) {
-            return;
+        if (isUxRestrictedOnDisplay(topTask.displayId)) {
+            doBlockTopActivityIfNotAllowed(topTask);
         }
-        if (!mUxRestrictionsListener.isRestricted()) {
-            return;
-        }
-        doBlockTopActivityIfNotAllowed(topTask);
     }
 
     private void doBlockTopActivityIfNotAllowed(TopTaskInfoContainer topTask) {
@@ -1004,8 +1073,9 @@
         }
 
         Intent newActivityIntent = createBlockingActivityIntent(
-                mActivityBlockingActivity, topTask.topActivity.flattenToShortString(),
-                topTask.taskId, taskRootActivity, isRootDO);
+                mActivityBlockingActivity, topTask.displayId,
+                topTask.topActivity.flattenToShortString(), topTask.taskId, taskRootActivity,
+                isRootDO);
 
         // Intent contains all info to debug what is blocked - log into both logcat and dumpsys.
         String log = "Starting blocking activity with intent: " + newActivityIntent.toUri(0);
@@ -1027,10 +1097,14 @@
      * @return an intent to launch the blocking activity.
      */
     private static Intent createBlockingActivityIntent(ComponentName blockingActivity,
-            String blockedActivity, int blockedTaskId, String taskRootActivity, boolean isRootDo) {
+            int displayId, String blockedActivity, int blockedTaskId, String taskRootActivity,
+            boolean isRootDo) {
         Intent newActivityIntent = new Intent();
+        newActivityIntent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
         newActivityIntent.setComponent(blockingActivity);
         newActivityIntent.putExtra(
+                BLOCKING_INTENT_EXTRA_DISPLAY_ID, displayId);
+        newActivityIntent.putExtra(
                 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME, blockedActivity);
         newActivityIntent.putExtra(
                 BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, blockedTaskId);
@@ -1042,21 +1116,6 @@
         return newActivityIntent;
     }
 
-    private void blockTopActivitiesIfNecessary() {
-        boolean restricted = mUxRestrictionsListener.isRestricted();
-        if (!restricted) {
-            return;
-        }
-        List<TopTaskInfoContainer> topTasks = mSystemActivityMonitoringService.getTopTasks();
-        for (TopTaskInfoContainer topTask : topTasks) {
-            if (topTask == null) {
-                Log.e(CarLog.TAG_PACKAGE, "Top tasks contains null.");
-                continue;
-            }
-            doBlockTopActivityIfNotAllowed(topTask);
-        }
-    }
-
     /**
      * Enable/Disable activity blocking by correspondingly enabling/disabling broadcasting UXR
      * changes in {@link CarUxRestrictionsManagerService}. This is only available in
@@ -1327,9 +1386,10 @@
                 }
             }
             if (DBG_POLICY_ENFORCEMENT) {
-                Log.d(CarLog.TAG_PACKAGE, "block?: " + shouldCheck);
+                Log.d(CarLog.TAG_PACKAGE, "Should check top tasks?: " + shouldCheck);
             }
             if (shouldCheck) {
+                // Loop over all top tasks to ensure tasks on virtual display can also be blocked.
                 blockTopActivitiesIfNecessary();
             }
         }
diff --git a/service/src/com/android/car/trust/BLEMessageV1Factory.java b/service/src/com/android/car/trust/BLEMessageV1Factory.java
index 42acafd..2478eca 100644
--- a/service/src/com/android/car/trust/BLEMessageV1Factory.java
+++ b/service/src/com/android/car/trust/BLEMessageV1Factory.java
@@ -88,8 +88,7 @@
             + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE)
             + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE);
 
-    private BLEMessageV1Factory() {
-    }
+    private BLEMessageV1Factory() {}
 
     /**
      * Method used to generate a single message, the packet number and total packets will set to 1
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
index 24a3b79..28a8fee 100644
--- a/service/src/com/android/car/trust/BleManager.java
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -22,6 +22,7 @@
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCallback;
 import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
 import android.bluetooth.BluetoothGattServer;
 import android.bluetooth.BluetoothGattServerCallback;
 import android.bluetooth.BluetoothGattService;
@@ -176,8 +177,14 @@
      */
     protected void notifyCharacteristicChanged(BluetoothDevice device,
             BluetoothGattCharacteristic characteristic, boolean confirm) {
-        if (mGattServer != null) {
-            mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+        if (mGattServer == null) {
+            return;
+        }
+
+        boolean result = mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "notifyCharacteristicChanged succeeded: " + result);
         }
     }
 
@@ -344,6 +351,19 @@
                 }
 
                 @Override
+                public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+                        BluetoothGattDescriptor descriptor, boolean preparedWrite,
+                        boolean responseNeeded, int offset, byte[] value) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Write request for descriptor: " + descriptor.getUuid()
+                                + "; value: " + Utils.byteArrayToHexString(value));
+                    }
+
+                    mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+                            offset, value);
+                }
+
+                @Override
                 public void onMtuChanged(BluetoothDevice device, int mtu) {
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
diff --git a/service/src/com/android/car/trust/CarBleTrustAgent.java b/service/src/com/android/car/trust/CarBleTrustAgent.java
index 772afdb..08035b8 100644
--- a/service/src/com/android/car/trust/CarBleTrustAgent.java
+++ b/service/src/com/android/car/trust/CarBleTrustAgent.java
@@ -134,9 +134,6 @@
             mCarTrustAgentUnlockService.stopUnlockAdvertising();
 
         }
-        // Set the trust state to false (not trusted), so unlocking is required for current user
-        // in case of user switch.
-        revokeTrust();
     }
 
     @Override
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
index f42c1e4..8517c98 100644
--- a/service/src/com/android/car/trust/CarTrustAgentBleManager.java
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
 import android.bluetooth.BluetoothGattService;
 import android.bluetooth.le.AdvertiseCallback;
 import android.bluetooth.le.AdvertiseData;
@@ -51,6 +52,16 @@
 
     private static final String TAG = "CarTrustBLEManager";
 
+    /**
+     * The UUID of the Client Characteristic Configuration Descriptor. This descriptor is
+     * responsible for specifying if a characteristic can be subscribed to for notifications.
+     *
+     * @see <a href="https://www.bluetooth.com/specifications/gatt/descriptors/">
+     *      GATT Descriptors</a>
+     */
+    private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
+            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+
     /** @hide */
     @IntDef(prefix = {"TRUSTED_DEVICE_OPERATION_"}, value = {
             TRUSTED_DEVICE_OPERATION_NONE,
@@ -83,8 +94,8 @@
 
     // Unlock Service and Characteristic UUIDs
     private UUID mUnlockServiceUuid;
-    private UUID mUnlockEscrowTokenUuid;
-    private UUID mUnlockTokenHandleUuid;
+    private UUID mUnlockClientWriteUuid;
+    private UUID mUnlockServerWriteUuid;
     private BluetoothGattService mUnlockGattService;
 
     private BLEMessagePayloadStream mBleMessagePayloadStream = new BLEMessagePayloadStream();
@@ -141,21 +152,18 @@
         if (!mBleMessagePayloadStream.isComplete()) {
             return;
         }
+
         if (uuid.equals(mEnrollmentClientWriteUuid)) {
             if (getEnrollmentService() != null) {
                 getEnrollmentService().onEnrollmentDataReceived(
                         mBleMessagePayloadStream.toByteArray());
             }
-        } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
+        } else if (uuid.equals(mUnlockClientWriteUuid)) {
             if (getUnlockService() != null) {
-                getUnlockService().onUnlockTokenReceived(mBleMessagePayloadStream.toByteArray());
-
-            }
-        } else if (uuid.equals(mUnlockTokenHandleUuid)) {
-            if (getUnlockService() != null) {
-                getUnlockService().onUnlockHandleReceived(mBleMessagePayloadStream.toByteArray());
+                getUnlockService().onUnlockDataReceived(mBleMessagePayloadStream.toByteArray());
             }
         }
+
         mBleMessagePayloadStream.reset();
     }
 
@@ -224,8 +232,8 @@
 
     /**
      * Setup the BLE GATT server for Enrollment. The GATT server for Enrollment comprises of one
-     * GATT Service and 2 characteristics - one for the escrow token to be generated and sent from
-     * the phone and the other for the handle generated and sent by the Head unit.
+     * GATT Service and 2 characteristics - one for the phone to write to and one for the head unit
+     * to write to.
      */
     void setupEnrollmentBleServer() {
         mEnrollmentServiceUuid = UUID.fromString(
@@ -238,18 +246,20 @@
         mEnrollmentGattService = new BluetoothGattService(mEnrollmentServiceUuid,
                 BluetoothGattService.SERVICE_TYPE_PRIMARY);
 
-        // Characteristic to describe the escrow token being used for unlock
+        // Characteristic the connected bluetooth device will write to.
         BluetoothGattCharacteristic clientCharacteristic =
                 new BluetoothGattCharacteristic(mEnrollmentClientWriteUuid,
                         BluetoothGattCharacteristic.PROPERTY_WRITE,
                         BluetoothGattCharacteristic.PERMISSION_WRITE);
 
-        // Characteristic to describe the handle being used for this escrow token
+        // Characteristic that this manager will write to.
         BluetoothGattCharacteristic serverCharacteristic =
                 new BluetoothGattCharacteristic(mEnrollmentServerWriteUuid,
                         BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                         BluetoothGattCharacteristic.PERMISSION_READ);
 
+        addDescriptorToCharacteristic(serverCharacteristic);
+
         mEnrollmentGattService.addCharacteristic(clientCharacteristic);
         mEnrollmentGattService.addCharacteristic(serverCharacteristic);
     }
@@ -257,32 +267,42 @@
     /**
      * Setup the BLE GATT server for Unlocking the Head unit. The GATT server for this phase also
      * comprises of 1 Service and 2 characteristics. However both the token and the handle are sent
-     * ftrom the phone to the head unit.
+     * from the phone to the head unit.
      */
     void setupUnlockBleServer() {
         mUnlockServiceUuid = UUID.fromString(getContext().getString(R.string.unlock_service_uuid));
-        mUnlockEscrowTokenUuid = UUID
-                .fromString(getContext().getString(R.string.unlock_escrow_token_uuid));
-        mUnlockTokenHandleUuid = UUID
-                .fromString(getContext().getString(R.string.unlock_handle_uuid));
+        mUnlockClientWriteUuid = UUID
+                .fromString(getContext().getString(R.string.unlock_client_write_uuid));
+        mUnlockServerWriteUuid = UUID
+                .fromString(getContext().getString(R.string.unlock_server_write_uuid));
 
         mUnlockGattService = new BluetoothGattService(mUnlockServiceUuid,
                 BluetoothGattService.SERVICE_TYPE_PRIMARY);
 
-        // Characteristic to describe the escrow token being used for unlock
-        BluetoothGattCharacteristic tokenCharacteristic = new BluetoothGattCharacteristic(
-                mUnlockEscrowTokenUuid,
+        // Characteristic the connected bluetooth device will write to.
+        BluetoothGattCharacteristic clientCharacteristic = new BluetoothGattCharacteristic(
+                mUnlockClientWriteUuid,
                 BluetoothGattCharacteristic.PROPERTY_WRITE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
 
-        // Characteristic to describe the handle being used for this escrow token
-        BluetoothGattCharacteristic handleCharacteristic = new BluetoothGattCharacteristic(
-                mUnlockTokenHandleUuid,
-                BluetoothGattCharacteristic.PROPERTY_WRITE,
-                BluetoothGattCharacteristic.PERMISSION_WRITE);
+        // Characteristic that this manager will write to.
+        BluetoothGattCharacteristic serverCharacteristic = new BluetoothGattCharacteristic(
+                mUnlockServerWriteUuid,
+                BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ);
 
-        mUnlockGattService.addCharacteristic(tokenCharacteristic);
-        mUnlockGattService.addCharacteristic(handleCharacteristic);
+        addDescriptorToCharacteristic(serverCharacteristic);
+
+        mUnlockGattService.addCharacteristic(clientCharacteristic);
+        mUnlockGattService.addCharacteristic(serverCharacteristic);
+    }
+
+    private void addDescriptorToCharacteristic(BluetoothGattCharacteristic characteristic) {
+        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(
+                CLIENT_CHARACTERISTIC_CONFIG,
+                BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
+        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+        characteristic.addDescriptor(descriptor);
     }
 
     void startEnrollmentAdvertising() {
@@ -340,28 +360,49 @@
         stopGattServer();
     }
 
-    /**
-     * Sends the given message to the specified device.
-     *
-     * @param device  The device to send the message to.
-     * @param message A message to send.
-     */
-    void sendMessage(BluetoothDevice device, byte[] message, OperationType operation,
+    void sendUnlockMessage(BluetoothDevice device, byte[] message, OperationType operation,
             boolean isPayloadEncrypted) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "sendMessage to: " + device.getAddress());
-        }
-        BluetoothGattCharacteristic serverCharacteristic = mEnrollmentGattService
+        BluetoothGattCharacteristic writeCharacteristic = mUnlockGattService
+                .getCharacteristic(mUnlockServerWriteUuid);
+
+        sendMessage(device, writeCharacteristic, message, operation, isPayloadEncrypted);
+    }
+
+    void sendEnrollmentMessage(BluetoothDevice device, byte[] message, OperationType operation,
+            boolean isPayloadEncrypted) {
+        BluetoothGattCharacteristic writeCharacteristic = mEnrollmentGattService
                 .getCharacteristic(mEnrollmentServerWriteUuid);
+
+        sendMessage(device, writeCharacteristic, message, operation, isPayloadEncrypted);
+    }
+
+    /**
+     * Sends the given message to the specified device and characteristic.
+     *
+     * @param device The device to send the message to.
+     * @param characteristic The characteristic to write to.
+     * @param message A message to send.
+     * @param operation The type of operation this message represents.
+     * @param isPayloadEncrypted {@code true} if the message is encrypted.
+     */
+    private void sendMessage(BluetoothDevice device, BluetoothGattCharacteristic characteristic,
+            byte[] message, OperationType operation, boolean isPayloadEncrypted) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "sendMessage to: " + device.getAddress() + "; and characteristic UUID: "
+                    + characteristic.getUuid());
+        }
+
         List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operation,
                 mMtuSize, isPayloadEncrypted);
+
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "sending " + bleMessages.size() + " messages to device");
         }
+
         for (BLEMessage bleMessage : bleMessages) {
             // TODO(b/131719066) get acknowledgement from the phone then continue to send packets
-            serverCharacteristic.setValue(bleMessage.toByteArray());
-            notifyCharacteristicChanged(device, serverCharacteristic, false);
+            characteristic.setValue(bleMessage.toByteArray());
+            notifyCharacteristicChanged(device, characteristic, false);
         }
     }
 
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 6743375..6c703e9 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -19,6 +19,9 @@
 import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_HANDSHAKE_FAILURE;
 import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_NOT_ALLOWED;
 
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.bluetooth.BluetoothDevice;
 import android.car.encryptionrunner.EncryptionRunner;
@@ -37,10 +40,6 @@
 import android.os.RemoteException;
 import android.util.Log;
 
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
 import com.android.car.R;
 import com.android.car.Utils;
@@ -183,7 +182,7 @@
     @Override
     public void enrollmentHandshakeAccepted(BluetoothDevice device) {
         addEnrollmentServiceLog("enrollmentHandshakeAccepted");
-        mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL,
+        mCarTrustAgentBleManager.sendEnrollmentMessage(device, CONFIRMATION_SIGNAL,
                 OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
         setEnrollmentHandshakeAccepted();
     }
@@ -412,6 +411,9 @@
             deviceName = mRemoteEnrollmentDevice.getName();
         } else if (mDeviceName != null) {
             deviceName = mDeviceName;
+            mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
+                    mEncryptionKey.encryptData(Utils.longToBytes(handle)),
+                    OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
         } else {
             deviceName = mContext.getString(R.string.trust_device_default_name);
         }
@@ -446,7 +448,7 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "Sending handle: " + handle);
         }
-        mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
+        mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
                 mEncryptionKey.encryptData(Utils.longToBytes(handle)),
                 OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
         dispatchEscrowTokenActiveStateChanged(handle, isTokenActive);
@@ -563,8 +565,9 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "Sending device id: " + uniqueId.toString());
         }
-        mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId),
-                OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ false);
+        mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
+                Utils.uuidToBytes(uniqueId), OperationType.CLIENT_MESSAGE,
+                /* isPayloadEncrypted= */ false);
         mEnrollmentState++;
     }
 
@@ -599,7 +602,7 @@
 
                 mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
                 mEncryptionState = mHandshakeMessage.getHandshakeState();
-                mCarTrustAgentBleManager.sendMessage(
+                mCarTrustAgentBleManager.sendEnrollmentMessage(
                         mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage(),
                         OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
 
@@ -626,7 +629,7 @@
                     showVerificationCode();
                     return;
                 }
-                mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
+                mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
                         mHandshakeMessage.getNextMessage(), OperationType.ENCRYPTION_HANDSHAKE,
                         /* isPayloadEncrypted= */ false);
                 break;
diff --git a/service/src/com/android/car/trust/CarTrustAgentUnlockService.java b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
index 8808f50..c0d90d4 100644
--- a/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
@@ -16,31 +16,109 @@
 
 package com.android.car.trust;
 
+import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.bluetooth.BluetoothDevice;
+import android.car.encryptionrunner.EncryptionRunner;
+import android.car.encryptionrunner.EncryptionRunnerFactory;
+import android.car.encryptionrunner.HandshakeException;
+import android.car.encryptionrunner.HandshakeMessage;
+import android.car.encryptionrunner.Key;
 import android.content.SharedPreferences;
 import android.util.Log;
 
+import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
 import com.android.car.Utils;
 import com.android.internal.annotations.GuardedBy;
 
+import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps;
+
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.LinkedList;
 import java.util.Queue;
+import java.util.UUID;
+
+import javax.crypto.spec.SecretKeySpec;
 
 /**
  * A service that interacts with the Trust Agent {@link CarBleTrustAgent} and a comms (BLE) service
  * {@link CarTrustAgentBleManager} to receive the necessary credentials to authenticate
  * an Android user.
+ *
+ * <p>
+ * The unlock flow is as follows:
+ * <ol>
+ * <li>IHU advertises via BLE when it is in a locked state.  The advertisement includes its
+ * identifier.
+ * <li>Phone (Trusted device) scans, finds and connects to the IHU.
+ * <li>Protocol versions are exchanged and verified.
+ * <li>Phone sends its identifier in plain text.
+ * <li>IHU verifies that the phone is enrolled as a trusted device from its identifier.
+ * <li>IHU, then sends an ACK back to the phone.
+ * <li>Phone & IHU go over the key exchange (using UKEY2) for encrypting this new session.
+ * <li>Key exchange is completed without any numeric comparison.
+ * <li>Phone sends its MAC (digest) that is computed from the context from this new session and the
+ * previous session.
+ * <li>IHU computes Phone's MAC and validates against what the phone sent.  On validation failure,
+ * the stored encryption keys for the phone are deleted.  This would require the phone to re-enroll
+ * again.
+ * <li>IHU sends its MAC that is computed similarly from the new session and previous session
+ * contexts.
+ * <li>Phone computes IHU's MAC internally and validates it against what it received.
+ * <li>At this point, the devices have mutually authenticated each other and also have keys to
+ * encrypt
+ * current session.
+ * <li>IHU saves the current session keys.  This would serve for authenticating the next session.
+ * <li>Phone sends the encrypted escrow token and handle to the IHU.
+ * <li>IHU retrieves the user id and authenticates the user.
+ * </ol>
  */
 public class CarTrustAgentUnlockService {
     private static final String TAG = "CarTrustAgentUnlock";
     private static final String TRUSTED_DEVICE_UNLOCK_ENABLED_KEY = "trusted_device_unlock_enabled";
-    //Arbirary log size
+
+    // Arbitrary log size
     private static final int MAX_LOG_SIZE = 20;
+    private static final byte[] RESUME = "RESUME".getBytes();
+    private static final byte[] SERVER = "SERVER".getBytes();
+    private static final byte[] CLIENT = "CLIENT".getBytes();
+    private static final int RESUME_HMAC_LENGTH = 32;
+
+    private static final byte[] ACKNOWLEDGEMENT_MESSAGE = "ACK".getBytes();
+
+    // State of the unlock process.  Important to maintain the same order in both phone and IHU.
+    // State increments to the next state on successful completion.
+    private static final int UNLOCK_STATE_WAITING_FOR_UNIQUE_ID = 0;
+    private static final int UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS = 1;
+    private static final int UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH = 2;
+    private static final int UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED = 3;
+    private static final int UNLOCK_STATE_TOKEN_RECEIVED = 4;
+    private static final int UNLOCK_STATE_HANDLE_RECEIVED = 5;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"UNLOCK_STATE_"}, value = {UNLOCK_STATE_WAITING_FOR_UNIQUE_ID,
+            UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS, UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH,
+            UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED, UNLOCK_STATE_TOKEN_RECEIVED,
+            UNLOCK_STATE_HANDLE_RECEIVED})
+    @interface UnlockState {
+    }
+
+    @UnlockState
+    private int mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+
     private final CarTrustedDeviceService mTrustedDeviceService;
     private final CarTrustAgentBleManager mCarTrustAgentBleManager;
     private CarTrustAgentUnlockDelegate mUnlockDelegate;
+    private String mClientDeviceId;
     private final Queue<String> mLogQueue = new LinkedList<>();
+
     // Locks
     private final Object mTokenLock = new Object();
     private final Object mHandleLock = new Object();
@@ -50,9 +128,19 @@
     private byte[] mUnlockToken;
     @GuardedBy("mHandleLock")
     private byte[] mUnlockHandle;
+
     @GuardedBy("mDeviceLock")
     private BluetoothDevice mRemoteUnlockDevice;
 
+    private EncryptionRunner mEncryptionRunner = EncryptionRunnerFactory.newRunner();
+    private HandshakeMessage mHandshakeMessage;
+    private Key mEncryptionKey;
+    @HandshakeMessage.HandshakeState
+    private int mEncryptionState = HandshakeMessage.HandshakeState.UNKNOWN;
+
+    private D2DConnectionContext mPrevContext;
+    private D2DConnectionContext mCurrentContext;
+
     CarTrustAgentUnlockService(CarTrustedDeviceService service,
             CarTrustAgentBleManager bleService) {
         mTrustedDeviceService = service;
@@ -78,7 +166,8 @@
      * Enable or disable authentication of the head unit with a trusted device.
      *
      * @param isEnabled when set to {@code false}, head unit will not be
-     * discoverable to unlock the user. Setting it to {@code true} will enable it back.
+     *                  discoverable to unlock the user. Setting it to {@code true} will enable it
+     *                  back.
      */
     public void setTrustedDeviceUnlockEnabled(boolean isEnabled) {
         SharedPreferences.Editor editor = mTrustedDeviceService.getSharedPrefs().edit();
@@ -87,6 +176,7 @@
             Log.wtf(TAG, "Unlock Enable Failed. Enable? " + isEnabled);
         }
     }
+
     /**
      * Set a delegate that implements {@link CarTrustAgentUnlockDelegate}. The delegate will be
      * handed the auth related data (token and handle) when it is received from the remote
@@ -129,6 +219,7 @@
         // Also disconnect from the peer.
         if (mRemoteUnlockDevice != null) {
             mCarTrustAgentBleManager.disconnectRemoteDevice();
+            mRemoteUnlockDevice = null;
         }
     }
 
@@ -140,6 +231,8 @@
         synchronized (mDeviceLock) {
             mRemoteUnlockDevice = null;
         }
+        mPrevContext = null;
+        mCurrentContext = null;
     }
 
     void onRemoteDeviceConnected(BluetoothDevice device) {
@@ -151,6 +244,7 @@
             queueMessageForLog("onRemoteDeviceConnected (addr:" + device.getAddress() + ")");
             mRemoteUnlockDevice = device;
         }
+        mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
     }
 
     void onRemoteDeviceDisconnected(BluetoothDevice device) {
@@ -162,38 +256,282 @@
         synchronized (mDeviceLock) {
             mRemoteUnlockDevice = null;
         }
+        mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+    }
+
+    void onUnlockDataReceived(byte[] value) {
+        switch (mCurrentUnlockState) {
+            case UNLOCK_STATE_WAITING_FOR_UNIQUE_ID:
+                mClientDeviceId = convertToDeviceId(value);
+                if (mClientDeviceId == null) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Phone not enrolled as a trusted device");
+                    }
+                    resetUnlockStateOnFailure();
+                    return;
+                }
+                sendAckToClient(/* isEncrypted = */ false);
+                // Next step is to wait for the client to start the encryption handshake.
+                mCurrentUnlockState = UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS;
+                break;
+            case UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS:
+                try {
+                    processKeyExchangeHandshakeMessage(value);
+                } catch (HandshakeException e) {
+                    Log.e(TAG, "Handshake failure", e);
+                    resetUnlockStateOnFailure();
+                }
+                break;
+            case UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH:
+                if (!authenticateClient(value)) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG,
+                                "HMAC from the phone is not correct. Cannot resume session.  Need"
+                                        + " to re-enroll");
+                    }
+                    mTrustedDeviceService.clearEncryptionKey(mClientDeviceId);
+                    resetUnlockStateOnFailure();
+
+                    return;
+                }
+                sendServerAuthToClient();
+                mCurrentUnlockState = UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED;
+                break;
+            case UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED:
+                if (mEncryptionKey == null) {
+                    Log.e(TAG, "Current session key null. Unexpected at this stage: "
+                            + mCurrentUnlockState);
+                    // Clear the previous session key.  Need to re-enroll the trusted device.
+                    mTrustedDeviceService.clearEncryptionKey(mClientDeviceId);
+                    resetUnlockStateOnFailure();
+                }
+                // Save the current session to be used for authenticating the next session
+                mTrustedDeviceService.saveEncryptionKey(mClientDeviceId, mEncryptionKey.asBytes());
+
+                onUnlockTokenReceived(value);
+                mCurrentUnlockState = UNLOCK_STATE_TOKEN_RECEIVED;
+                // Let the phone know that the token was received.
+                sendAckToClient(/* isEncrypted = */ true);
+                break;
+            // TODO(b/131124919) Combine token and handle in the same packet
+            case UNLOCK_STATE_TOKEN_RECEIVED:
+                onUnlockHandleReceived(value);
+                mCurrentUnlockState = UNLOCK_STATE_HANDLE_RECEIVED;
+                break;
+            case UNLOCK_STATE_HANDLE_RECEIVED:
+                // Should never get here because the unlock process should be completed now.
+                Log.e(TAG, "Landed on unexpected state: " + mCurrentUnlockState);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void sendAckToClient(boolean isEncrypted) {
+        // Let the phone know that the handle was received.
+        byte[] ack = isEncrypted ? mEncryptionKey.encryptData(ACKNOWLEDGEMENT_MESSAGE)
+                : ACKNOWLEDGEMENT_MESSAGE;
+        mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice, ack,
+                OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ isEncrypted);
+    }
+
+    @Nullable
+    private String convertToDeviceId(byte[] id) {
+        // Validate if the id exists i.e., if the phone is enrolled already
+        UUID deviceId = Utils.bytesToUUID(id);
+        if (deviceId == null
+                || mTrustedDeviceService.getEncryptionKey(deviceId.toString()) == null) {
+            if (deviceId != null) {
+                Log.e(TAG, "Unknown phone connected: " + deviceId.toString());
+            }
+            return null;
+        }
+
+        return deviceId.toString();
+    }
+
+    private void processKeyExchangeHandshakeMessage(byte[] message) throws HandshakeException {
+        switch (mEncryptionState) {
+            case HandshakeMessage.HandshakeState.UNKNOWN:
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Responding to handshake init request.");
+                }
+
+                mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
+                mEncryptionState = mHandshakeMessage.getHandshakeState();
+                mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice,
+                        mHandshakeMessage.getNextMessage(),
+                        OperationType.ENCRYPTION_HANDSHAKE,
+                        /* isPayloadEncrypted= */ false);
+
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Updated encryption state: " + mEncryptionState);
+                }
+                break;
+
+            case HandshakeMessage.HandshakeState.IN_PROGRESS:
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Continuing handshake.");
+                }
+
+                mHandshakeMessage = mEncryptionRunner.continueHandshake(message);
+                mEncryptionState = mHandshakeMessage.getHandshakeState();
+
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Updated encryption state: " + mEncryptionState);
+                }
+
+                // The state is updated after a call to continueHandshake(). Thus, need to check
+                // if we're in the next stage.
+                if (mEncryptionState == HandshakeMessage.HandshakeState.VERIFICATION_NEEDED) {
+                    showVerificationCode();
+                    return;
+                }
+
+                // control shouldn't get here with Ukey2
+                mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice,
+                        mHandshakeMessage.getNextMessage(),
+                        OperationType.ENCRYPTION_HANDSHAKE, /*isPayloadEncrypted= */false);
+                break;
+            case HandshakeMessage.HandshakeState.VERIFICATION_NEEDED:
+            case HandshakeMessage.HandshakeState.FINISHED:
+                // Should never reach this case since this state should occur after a verification
+                // code has been accepted. But it should mean handshake is done and the message
+                // is one for the escrow token. Start Mutual Auth from server - compute MACs and
+                // send it over
+                showVerificationCode();
+                break;
+
+            default:
+                Log.w(TAG, "Encountered invalid handshake state: " + mEncryptionState);
+                break;
+        }
+    }
+
+    /**
+     * Verify the handshake.
+     * TODO(b/134073741) combine this with the method in CarTrustAgentEnrollmentService and
+     * have this take a boolean to blindly confirm the numeric code.
+     */
+    private void showVerificationCode() {
+        HandshakeMessage handshakeMessage;
+
+        // Blindly accept the verification code.
+        try {
+            handshakeMessage = mEncryptionRunner.verifyPin();
+        } catch (HandshakeException e) {
+            Log.e(TAG, "Verify pin failed for new keys - Unexpected");
+            resetUnlockStateOnFailure();
+            return;
+        }
+
+        if (handshakeMessage.getHandshakeState() != HandshakeMessage.HandshakeState.FINISHED) {
+            Log.e(TAG, "Handshake not finished after calling verify PIN. Instead got state: "
+                    + handshakeMessage.getHandshakeState());
+            resetUnlockStateOnFailure();
+            return;
+        }
+
+        mEncryptionState = HandshakeMessage.HandshakeState.FINISHED;
+        mEncryptionKey = handshakeMessage.getKey();
+        mCurrentContext = D2DConnectionContext.fromSavedSession(mEncryptionKey.asBytes());
+
+        if (mClientDeviceId == null) {
+            resetUnlockStateOnFailure();
+            return;
+        }
+        byte[] oldSessionKeyBytes = mTrustedDeviceService.getEncryptionKey(mClientDeviceId);
+        if (oldSessionKeyBytes == null) {
+            Log.e(TAG,
+                    "Could not retrieve previous session keys! Have to re-enroll trusted device");
+            resetUnlockStateOnFailure();
+            return;
+        }
+
+        mPrevContext = D2DConnectionContext.fromSavedSession(oldSessionKeyBytes);
+        if (mPrevContext == null) {
+            resetUnlockStateOnFailure();
+            return;
+        }
+
+        // Now wait for the phone to send its MAC.
+        mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH;
+    }
+
+    private void sendServerAuthToClient() {
+        byte[] resumeBytes = computeMAC(mPrevContext, mCurrentContext, SERVER);
+        if (resumeBytes == null) {
+            return;
+        }
+        // send to client
+        mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice, resumeBytes,
+                OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */false);
+    }
+
+    @Nullable
+    private byte[] computeMAC(D2DConnectionContext previous, D2DConnectionContext next,
+            byte[] info) {
+        try {
+            SecretKeySpec inputKeyMaterial = new SecretKeySpec(
+                    Utils.concatByteArrays(previous.getSessionUnique(), next.getSessionUnique()),
+                    "" /* key type is just plain raw bytes */);
+            return CryptoOps.hkdf(inputKeyMaterial, RESUME, info);
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            // Does not happen in practice
+            Log.e(TAG, "Compute MAC failed");
+            return null;
+        }
+    }
+
+    private boolean authenticateClient(byte[] message) {
+        if (message.length != RESUME_HMAC_LENGTH) {
+            Log.e(TAG, "failing because message.length is " + message.length);
+            return false;
+        }
+        return MessageDigest.isEqual(message,
+                computeMAC(mPrevContext, mCurrentContext, CLIENT));
     }
 
     void onUnlockTokenReceived(byte[] value) {
         synchronized (mTokenLock) {
             mUnlockToken = value;
         }
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Unlock Token: " + Utils.byteArrayToHexString(mUnlockToken));
-        }
-        queueMessageForLog("onUnlockTokenReceived");
-        if (mUnlockToken == null || mUnlockHandle == null) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Unlock Handle not available yet");
-            }
-            return;
-        }
-        if (mUnlockDelegate == null) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "No Unlock delegate");
-            }
-            return;
-        }
-        mUnlockDelegate.onUnlockDataReceived(
-                mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
-                mUnlockToken,
-                Utils.bytesToLong(mUnlockHandle));
 
-        synchronized (mTokenLock) {
-            mUnlockToken = null;
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Unlock Token received: " + Utils.byteArrayToHexString(mUnlockToken));
         }
-        synchronized (mHandleLock) {
-            mUnlockHandle = null;
+
+        queueMessageForLog("onUnlockTokenReceived");
+    }
+
+    /**
+     * Reset the whole unlock state.  Disconnects from the peer device
+     *
+     * <p>This method should be called from any stage in the middle of unlock where we
+     * encounter a failure.
+     */
+    private void resetUnlockStateOnFailure() {
+        mCarTrustAgentBleManager.disconnectRemoteDevice();
+        resetEncryptionState();
+    }
+
+    /**
+     * Resets the encryption status of this service.
+     *
+     * <p>This method should be called each time a device connects so that a new handshake can be
+     * started and encryption keys exchanged.
+     */
+    private void resetEncryptionState() {
+        mEncryptionRunner = EncryptionRunnerFactory.newRunner();
+        mHandshakeMessage = null;
+        mEncryptionKey = null;
+        mEncryptionState = HandshakeMessage.HandshakeState.UNKNOWN;
+        mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+        if (mCurrentContext != null) {
+            mCurrentContext = null;
+        }
+        if (mPrevContext != null) {
+            mPrevContext = null;
         }
     }
 
@@ -201,10 +539,13 @@
         synchronized (mHandleLock) {
             mUnlockHandle = value;
         }
+
         if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Unlock Handle: " + Utils.byteArrayToHexString(mUnlockHandle));
+            Log.d(TAG, "Unlock Handl received: " + Utils.byteArrayToHexString(mUnlockHandle));
         }
+
         queueMessageForLog("onUnlockHandleReceived");
+
         if (mUnlockToken == null || mUnlockHandle == null) {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "Unlock Token not available yet");
@@ -218,6 +559,7 @@
             }
             return;
         }
+
         mUnlockDelegate.onUnlockDataReceived(
                 mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
                 mUnlockToken,
diff --git a/service/src/com/android/car/trust/CarTrustedDeviceService.java b/service/src/com/android/car/trust/CarTrustedDeviceService.java
index 9995aa7..cc79eff 100644
--- a/service/src/com/android/car/trust/CarTrustedDeviceService.java
+++ b/service/src/com/android/car/trust/CarTrustedDeviceService.java
@@ -16,6 +16,7 @@
 
 package com.android.car.trust;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.bluetooth.BluetoothDevice;
 import android.car.trust.TrustedDeviceInfo;
@@ -26,8 +27,6 @@
 import android.util.Base64;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
-
 import com.android.car.CarServiceBase;
 import com.android.car.R;
 import com.android.car.Utils;
@@ -51,6 +50,7 @@
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.KeyGenerator;
 import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
 
 /**
  * The part of the Car service that enables the Trusted device feature.  Trusted Device is a feature
@@ -63,12 +63,21 @@
  */
 public class CarTrustedDeviceService implements CarServiceBase {
     private static final String TAG = CarTrustedDeviceService.class.getSimpleName();
+
     private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
     private static final String PREF_ENCRYPTION_KEY_PREFIX = "CTABM_encryption_key";
     private static final String KEY_ALIAS = "Ukey2Key";
     private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
     private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
+    private static final String IV_SPEC_SEPARATOR = ";";
+
+    // The length of the authentication tag for a cipher in GCM mode. The GCM specification states
+    // that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
+    // possible value.
+    private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
+
     private static final int RANDOM_NAME_LENGTH = 6;
+
     private final Context mContext;
     private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
     private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
@@ -222,13 +231,21 @@
     @Nullable
     byte[] getEncryptionKey(String deviceId) {
         SharedPreferences prefs = getSharedPrefs();
-        if (!prefs.contains(deviceId)) {
+        String key = PREF_ENCRYPTION_KEY_PREFIX + deviceId;
+        if (!prefs.contains(key)) {
             return null;
         }
-        byte[] encryptedKey = Base64.decode(
-                prefs.getString(PREF_ENCRYPTION_KEY_PREFIX + deviceId, null),
-                Base64.DEFAULT);
-        return decryptWithKeyStore(KEY_ALIAS, encryptedKey);
+
+        // This value will not be "null" because we already checked via a call to contains().
+        String[] values = prefs.getString(key, null).split(IV_SPEC_SEPARATOR);
+
+        if (values.length != 2) {
+            return null;
+        }
+
+        byte[] encryptedKey = Base64.decode(values[0], Base64.DEFAULT);
+        byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
+        return decryptWithKeyStore(KEY_ALIAS, encryptedKey, ivSpec);
     }
 
     /**
@@ -238,20 +255,37 @@
      * @param encryptionKey encryption key
      * @return {@code true} if the operation succeeded
      */
-    boolean saveEncryptionKey(String deviceId, byte[] encryptionKey) {
-        byte[] encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
+    boolean saveEncryptionKey(@Nullable String deviceId, @Nullable byte[] encryptionKey) {
+        if (encryptionKey == null || deviceId == null) {
+            return false;
+        }
+        String encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
         if (encryptedKey == null) {
             return false;
         }
+        if (getSharedPrefs().contains(deviceId)) {
+            clearEncryptionKey(deviceId);
+        }
 
         return getSharedPrefs()
                 .edit()
-                .putString(PREF_ENCRYPTION_KEY_PREFIX + deviceId,
-                        Base64.encodeToString(encryptedKey, Base64.DEFAULT))
+                .putString(PREF_ENCRYPTION_KEY_PREFIX + deviceId, encryptedKey)
                 .commit();
     }
 
     /**
+     * Clear the encryption key for the given device
+     *
+     * @param deviceId id of the peer device
+     */
+    void clearEncryptionKey(@Nullable String deviceId) {
+        if (deviceId == null) {
+            return;
+        }
+        getSharedPrefs().edit().remove(deviceId);
+    }
+
+    /**
      * Get generated random name for enrollment
      *
      * @return a random name for enrollment
@@ -268,12 +302,18 @@
     /**
      * Encrypt value with designated key
      *
+     * <p>The encrypted value is of the form:
+     *
+     * <p>key + IV_SPEC_SEPARATOR + ivSpec
+     *
+     * <p>The {@code ivSpec} is needed to decrypt this key later on.
+     *
      * @param keyAlias KeyStore alias for key to use
      * @param value a value to encrypt
      * @return encrypted value, null if unable to encrypt
      */
     @Nullable
-    byte[] encryptWithKeyStore(String keyAlias, byte[] value) {
+    String encryptWithKeyStore(String keyAlias, byte[] value) {
         if (value == null) {
             return null;
         }
@@ -282,7 +322,10 @@
         try {
             Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
             cipher.init(Cipher.ENCRYPT_MODE, key);
-            return cipher.doFinal(value);
+            return new StringBuffer(Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT))
+                .append(IV_SPEC_SEPARATOR)
+                .append(Base64.encodeToString(cipher.getIV(), Base64.DEFAULT))
+                .toString();
         } catch (IllegalBlockSizeException
                 | BadPaddingException
                 | NoSuchAlgorithmException
@@ -302,7 +345,7 @@
      * @return decrypted value, null if unable to decrypt
      */
     @Nullable
-    byte[] decryptWithKeyStore(String keyAlias, byte[] value) {
+    byte[] decryptWithKeyStore(String keyAlias, byte[] value, byte[] ivSpec) {
         if (value == null) {
             return null;
         }
@@ -310,14 +353,16 @@
         try {
             Key key = getKeyStoreKey(keyAlias);
             Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
-            cipher.init(Cipher.DECRYPT_MODE, key);
+            cipher.init(Cipher.DECRYPT_MODE, key,
+                    new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
             return cipher.doFinal(value);
         } catch (IllegalBlockSizeException
                 | BadPaddingException
                 | NoSuchAlgorithmException
                 | NoSuchPaddingException
                 | IllegalStateException
-                | InvalidKeyException e) {
+                | InvalidKeyException
+                | InvalidAlgorithmParameterException e) {
             Log.e(TAG, "Unable to decrypt value with key " + keyAlias, e);
             return null;
         }
diff --git a/service/src/com/android/car/vms/VmsBrokerService.java b/service/src/com/android/car/vms/VmsBrokerService.java
index 216c8fd..26626ff 100644
--- a/service/src/com/android/car/vms/VmsBrokerService.java
+++ b/service/src/com/android/car/vms/VmsBrokerService.java
@@ -22,19 +22,24 @@
 import android.car.vms.VmsLayersOffering;
 import android.car.vms.VmsOperationRecorder;
 import android.car.vms.VmsSubscriptionState;
+import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.IBinder;
+import android.os.Process;
 import android.util.Log;
 
 import com.android.car.VmsLayersAvailability;
 import com.android.car.VmsPublishersInfo;
 import com.android.car.VmsRouting;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.IntSupplier;
 
 /**
  * Broker service facilitating subscription handling and message passing between
@@ -44,15 +49,26 @@
     private static final boolean DBG = true;
     private static final String TAG = "VmsBrokerService";
 
+    @VisibleForTesting
+    static final String HAL_CLIENT = "HalClient";
+
+    @VisibleForTesting
+    static final String UNKNOWN_PACKAGE = "UnknownPackage";
+
     private CopyOnWriteArrayList<PublisherListener> mPublisherListeners =
             new CopyOnWriteArrayList<>();
     private CopyOnWriteArrayList<SubscriberListener> mSubscriberListeners =
             new CopyOnWriteArrayList<>();
+    private PackageManager mPackageManager;
+    private IntSupplier mGetCallingPid;
+    private IntSupplier mGetCallingUid;
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private final VmsRouting mRouting = new VmsRouting();
     @GuardedBy("mLock")
+    private final Map<IBinder, String> mBinderPackage = new HashMap<>();
+    @GuardedBy("mLock")
     private final Map<IBinder, Map<Integer, VmsLayersOffering>> mOfferings = new HashMap<>();
     @GuardedBy("mLock")
     private final VmsLayersAvailability mAvailableLayers = new VmsLayersAvailability();
@@ -95,8 +111,17 @@
     /**
      * Constructs new broker service.
      */
-    public VmsBrokerService() {
+    public VmsBrokerService(PackageManager packageManager) {
+        this(packageManager, Binder::getCallingPid, Binder::getCallingUid);
+    }
+
+    @VisibleForTesting
+    VmsBrokerService(PackageManager packageManager, IntSupplier getCallingPid,
+            IntSupplier getCallingUid) {
         if (DBG) Log.d(TAG, "Started VmsBrokerService!");
+        mPackageManager = packageManager;
+        mGetCallingPid = getCallingPid;
+        mGetCallingUid = getCallingUid;
     }
 
     /**
@@ -143,6 +168,8 @@
     public void addSubscription(IVmsSubscriberClient subscriber) {
         synchronized (mLock) {
             mRouting.addSubscription(subscriber);
+            // Add mapping from binder to package name of subscriber.
+            mBinderPackage.computeIfAbsent(subscriber.asBinder(), k -> getCallingPackage());
         }
     }
 
@@ -172,6 +199,9 @@
 
             // Add the listeners subscription to the layer
             mRouting.addSubscription(subscriber, layer);
+
+            // Add mapping from binder to package name of subscriber.
+            mBinderPackage.computeIfAbsent(subscriber.asBinder(), k -> getCallingPackage());
         }
         if (firstSubscriptionForLayer) {
             notifyOfSubscriptionChange();
@@ -219,6 +249,9 @@
 
             // Add the listeners subscription to the layer
             mRouting.addSubscription(subscriber, layer, publisherId);
+
+            // Add mapping from binder to package name of subscriber.
+            mBinderPackage.computeIfAbsent(subscriber.asBinder(), k -> getCallingPackage());
         }
         if (firstSubscriptionForLayer) {
             notifyOfSubscriptionChange();
@@ -263,6 +296,9 @@
         boolean subscriptionStateChanged;
         synchronized (mLock) {
             subscriptionStateChanged = mRouting.removeDeadSubscriber(subscriber);
+
+            // Remove mapping from binder to package name of subscriber.
+            mBinderPackage.remove(subscriber.asBinder());
         }
         if (subscriptionStateChanged) {
             notifyOfSubscriptionChange();
@@ -358,6 +394,15 @@
         }
     }
 
+    /**
+     * Gets the package name for a given IVmsSubscriberClient
+     */
+    public String getPackageName(IVmsSubscriberClient subscriber) {
+        synchronized (mLock) {
+            return mBinderPackage.get(subscriber.asBinder());
+        }
+    }
+
     private void updateLayerAvailability() {
         Set<VmsLayersOffering> allPublisherOfferings = new HashSet<>();
         synchronized (mLock) {
@@ -388,4 +433,21 @@
             listener.onLayersAvailabilityChange(availableLayers);
         }
     }
+
+    // If we're in a binder call, returns back the package name of the caller of the binder call.
+    private String getCallingPackage() {
+        int callingPid = mGetCallingPid.getAsInt();
+        // Since the HAL lives in the same process, if the callingPid is equal to this process's
+        // PID, we know it's the HAL client.
+        if (callingPid == Process.myPid()) {
+            return HAL_CLIENT;
+        }
+        int callingUid = mGetCallingUid.getAsInt();
+        String packageName = mPackageManager.getNameForUid(callingUid);
+        if (packageName == null) {
+            return UNKNOWN_PACKAGE;
+        } else {
+            return packageName;
+        }
+    }
 }
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 14ef0a7..d3c39ff 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -40,6 +40,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * Manages service connections lifecycle for VMS publisher clients.
@@ -92,6 +93,9 @@
     @GuardedBy("mCurrentUserClients")
     private int mCurrentUser;
 
+    @GuardedBy("mRebindCounts")
+    private final Map<String, AtomicLong> mRebindCounts = new ArrayMap<>();
+
     @VisibleForTesting
     final Runnable mSystemUserUnlockedListener = () -> {
         synchronized (mSystemClients) {
@@ -164,10 +168,30 @@
     @Override
     public void dump(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
-        writer.println("mListeners:" + mListeners);
-        writer.println("mSystemClients:" + mSystemClients.keySet());
-        writer.println("mCurrentUser:" + mCurrentUser);
-        writer.println("mCurrentUserClients:" + mCurrentUserClients.keySet());
+        synchronized (mSystemClients) {
+            writer.println("mHalClient: " + (mHalClient != null ? "connected" : "disconnected"));
+            writer.println("mSystemClients:");
+            dumpConnections(writer, mSystemClients);
+        }
+        synchronized (mCurrentUserClients) {
+            writer.println("mCurrentUserClients:");
+            dumpConnections(writer, mCurrentUserClients);
+            writer.println("mCurrentUser:" + mCurrentUser);
+        }
+        synchronized (mRebindCounts) {
+            writer.println("mRebindCounts:");
+            for (Map.Entry<String, AtomicLong> entry : mRebindCounts.entrySet()) {
+                writer.printf("\t%s: %s\n", entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    private void dumpConnections(PrintWriter writer, Map<String, ClientConnection> connectionMap) {
+        for (ClientConnection connection : connectionMap.values()) {
+            writer.printf("\t%s: %s\n",
+                    connection.mName.getPackageName(),
+                    connection.mIsBound ? "connected" : "disconnected");
+        }
     }
 
     /**
@@ -313,6 +337,9 @@
             mHalClient = null;
             notifyListenersOnClientDisconnected(HAL_CLIENT_NAME);
         }
+        synchronized (mRebindCounts) {
+            mRebindCounts.computeIfAbsent(HAL_CLIENT_NAME, k -> new AtomicLong()).incrementAndGet();
+        }
     }
 
     class ClientConnection implements ServiceConnection {
@@ -376,6 +403,10 @@
             }
             if (!mIsTerminated) {
                 mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+                synchronized (mRebindCounts) {
+                    mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
+                            .incrementAndGet();
+                }
             }
         }
 
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarHvacManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarHvacManagerTest.java
index 2fc57eb..7a3b7b5 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarHvacManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarHvacManagerTest.java
@@ -120,13 +120,13 @@
 
             for (int areaId : areaIds) {
                 assertTrue(property.hasArea(areaId));
-                int min = property.getMinValue(areaId);
-                int max = property.getMaxValue(areaId);
+                int min = property.getMinValue(areaId) == null ? 0 : property.getMinValue(areaId);
+                int max = property.getMaxValue(areaId) == null ? 0 : property.getMaxValue(areaId);
                 assertTrue(min <= max);
             }
         } else {
-            int min = property.getMinValue();
-            int max = property.getMaxValue();
+            int min = property.getMinValue() == null ? 0 : property.getMinValue();
+            int max = property.getMaxValue() == null ? 0 : property.getMinValue();
             assertTrue(min <= max);
             for (int i = 0; i < 32; i++) {
                 assertFalse(property.hasArea(0x1 << i));
@@ -143,15 +143,17 @@
             assertTrue(areaIds.length > 0);
             assertEquals(areaIds.length, property.getAreaCount());
 
-            for (int areId : areaIds) {
-                assertTrue(property.hasArea(areId));
-                float min = property.getMinValue(areId);
-                float max = property.getMaxValue(areId);
+            for (int areaId : areaIds) {
+                assertTrue(property.hasArea(areaId));
+                float min =
+                        property.getMinValue(areaId) == null ? 0f : property.getMinValue(areaId);
+                float max =
+                        property.getMaxValue(areaId) == null ? 0f : property.getMinValue(areaId);
                 assertTrue(min <= max);
             }
         } else {
-            float min = property.getMinValue();
-            float max = property.getMaxValue();
+            float min = property.getMinValue() == null ? 0f : property.getMinValue();
+            float max = property.getMaxValue() == null ? 0f : property.getMinValue();
             assertTrue(min <= max);
             for (int i = 0; i < 32; i++) {
                 assertFalse(property.hasArea(0x1 << i));
diff --git a/tests/carservice_test/src/com/android/car/vms/VmsBrokerServiceTest.java b/tests/carservice_test/src/com/android/car/vms/VmsBrokerServiceTest.java
new file mode 100644
index 0000000..5ce7df5
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/vms/VmsBrokerServiceTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.vms;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.vms.IVmsSubscriberClient;
+import android.car.vms.VmsLayer;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Process;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Test;
+
+import java.util.function.IntSupplier;
+
+@MediumTest
+public class VmsBrokerServiceTest {
+
+    class MockIntProvider implements IntSupplier {
+        private int[] mInts;
+        private int mIdx;
+
+        MockIntProvider(int... ints) {
+            mInts = ints;
+            mIdx = 0;
+        }
+
+        public int getAsInt() {
+            int ret = mInts[mIdx];
+            mIdx++;
+            return ret;
+        }
+    }
+
+    /**
+     * Test that adding a subscriber to VmsBrokerService also keeps track of the package name for
+     * a given subscriber. Also checks that if we remove a dead subscriber, we no longer track the
+     * package name associated with it.
+     */
+    @Test
+    public void testAddSubscription() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+        Binder binder = mock(Binder.class);
+        VmsLayer layer = mock(VmsLayer.class);
+        when(packageManager.getNameForUid(0)).thenReturn("test.package1");
+        when(subscriberClient.asBinder()).thenReturn(binder);
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 200, () -> 0);
+        broker.addSubscription(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isEqualTo("test.package1");
+        broker.removeDeadSubscriber(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isNull();
+    }
+
+    @Test
+    public void testAddSubscriptionLayer() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+        Binder binder = mock(Binder.class);
+        VmsLayer layer = mock(VmsLayer.class);
+        when(packageManager.getNameForUid(0)).thenReturn("test.package2");
+        when(subscriberClient.asBinder()).thenReturn(binder);
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 200, () -> 0);
+        broker.addSubscription(subscriberClient, layer);
+        assertThat(broker.getPackageName(subscriberClient)).isEqualTo("test.package2");
+        broker.removeDeadSubscriber(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isNull();
+    }
+
+    @Test
+    public void testAddSubscriptionLayerVersion() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+        Binder binder = mock(Binder.class);
+        VmsLayer layer = mock(VmsLayer.class);
+        when(packageManager.getNameForUid(0)).thenReturn("test.package3");
+        when(subscriberClient.asBinder()).thenReturn(binder);
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 200, () -> 0);
+        broker.addSubscription(subscriberClient, layer, 1234);
+        assertThat(broker.getPackageName(subscriberClient)).isEqualTo("test.package3");
+        broker.removeDeadSubscriber(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isNull();
+    }
+
+    @Test
+    public void testMultipleSubscriptionsSameClientCallsPackageManagerOnce() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+        Binder binder = mock(Binder.class);
+        when(subscriberClient.asBinder()).thenReturn(binder);
+        when(packageManager.getNameForUid(0)).thenReturn("test.package3");
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 0, () -> 0);
+        broker.addSubscription(subscriberClient);
+        broker.addSubscription(subscriberClient);
+        // The second argument isn't necessary but is here for clarity.
+        verify(packageManager, times(1)).getNameForUid(0);
+    }
+
+    @Test
+    public void testUnknownPackageName() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+        Binder binder = mock(Binder.class);
+        when(subscriberClient.asBinder()).thenReturn(binder);
+        when(packageManager.getNameForUid(0)).thenReturn(null);
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 0, () -> 0);
+        broker.addSubscription(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isEqualTo(
+                VmsBrokerService.UNKNOWN_PACKAGE);
+    }
+
+    /**
+     * Tests that if the HAL is a subscriber, we record its package name as HalClient.
+     */
+    @Test
+    public void testAddingHalSubscriberSavesPackageName() {
+        PackageManager packageManager = mock(PackageManager.class);
+        IVmsSubscriberClient subscriberClient = mock(IVmsSubscriberClient.class);
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> Process.myPid(),
+                () -> Process.SYSTEM_UID);
+        broker.addSubscription(subscriberClient);
+        assertThat(broker.getPackageName(subscriberClient)).isEqualTo(VmsBrokerService.HAL_CLIENT);
+    }
+
+    @Test
+    public void testMultipleSubscriptionsPackageManager() {
+        PackageManager packageManager = mock(PackageManager.class);
+
+        IVmsSubscriberClient subscriberClient1 = mock(IVmsSubscriberClient.class);
+        Binder binder1 = mock(Binder.class);
+        when(subscriberClient1.asBinder()).thenReturn(binder1);
+
+        IVmsSubscriberClient subscriberClient2 = mock(IVmsSubscriberClient.class);
+        Binder binder2 = mock(Binder.class);
+        when(subscriberClient2.asBinder()).thenReturn(binder2);
+
+        IVmsSubscriberClient subscriberClient3 = mock(IVmsSubscriberClient.class);
+        Binder binder3 = mock(Binder.class);
+        when(subscriberClient3.asBinder()).thenReturn(binder3);
+
+        // Tests a client with a different UID but the same package as subscriberClient1
+        IVmsSubscriberClient subscriberClient4 = mock(IVmsSubscriberClient.class);
+        Binder binder4 = mock(Binder.class);
+        when(subscriberClient4.asBinder()).thenReturn(binder4);
+
+        when(packageManager.getNameForUid(0)).thenReturn("test.package0");
+        when(packageManager.getNameForUid(1)).thenReturn("test.package1");
+        when(packageManager.getNameForUid(2)).thenReturn("test.package2");
+        when(packageManager.getNameForUid(3)).thenReturn("test.package0");
+
+        VmsBrokerService broker = new VmsBrokerService(packageManager, () -> 10,
+                new MockIntProvider(0, 1, 2, 3));
+
+        broker.addSubscription(subscriberClient1);
+        broker.addSubscription(subscriberClient2);
+        broker.addSubscription(subscriberClient3);
+        broker.addSubscription(subscriberClient4);
+
+        verify(packageManager).getNameForUid(0);
+        verify(packageManager).getNameForUid(1);
+        verify(packageManager).getNameForUid(2);
+        verify(packageManager).getNameForUid(3);
+
+        assertThat(broker.getPackageName(subscriberClient1)).isEqualTo("test.package0");
+        assertThat(broker.getPackageName(subscriberClient2)).isEqualTo("test.package1");
+        assertThat(broker.getPackageName(subscriberClient3)).isEqualTo("test.package2");
+        assertThat(broker.getPackageName(subscriberClient4)).isEqualTo("test.package0");
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
index 4290354..d08d32a 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
@@ -28,6 +28,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
@@ -73,6 +75,7 @@
  * 2. {@link LocationManager} provides dummy {@link Location}s.
  * 3. {@link CarUserManagerHelper} tells whether or not the system user is headless.
  * 4. {@link SystemInterface} tells where to store system files.
+ * 5. {@link CarDrivingStateService} tells about driving state changes.
  */
 @RunWith(AndroidJUnit4.class)
 public class CarLocationServiceTest {
@@ -90,6 +93,8 @@
     private CarUserManagerHelper mMockCarUserManagerHelper;
     @Mock
     private SystemInterface mMockSystemInterface;
+    @Mock
+    private CarDrivingStateService mMockCarDrivingStateService;
 
     /**
      * Initialize all of the objects with the @Mock annotation.
@@ -111,6 +116,9 @@
         };
         CarLocalServices.removeServiceForTest(SystemInterface.class);
         CarLocalServices.addService(SystemInterface.class, mMockSystemInterface);
+        CarLocalServices.removeServiceForTest(CarDrivingStateService.class);
+        CarLocalServices.addService(CarDrivingStateService.class, mMockCarDrivingStateService);
+        when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
     }
 
     @After
@@ -142,10 +150,13 @@
      */
     @Test
     public void testRegistersToReceiveEvents() {
-        ArgumentCaptor<IntentFilter> argument = ArgumentCaptor.forClass(IntentFilter.class);
+        ArgumentCaptor<IntentFilter> intentFilterArgument = ArgumentCaptor.forClass(
+                IntentFilter.class);
         mCarLocationService.init();
-        verify(mMockContext).registerReceiver(eq(mCarLocationService), argument.capture());
-        IntentFilter intentFilter = argument.getValue();
+        verify(mMockContext).registerReceiver(eq(mCarLocationService),
+                intentFilterArgument.capture());
+        verify(mMockCarDrivingStateService).registerDrivingStateChangeListener(any());
+        IntentFilter intentFilter = intentFilterArgument.getValue();
         assertEquals(3, intentFilter.countActions());
         String[] actions = {intentFilter.getAction(0), intentFilter.getAction(1),
                 intentFilter.getAction(2)};
@@ -159,8 +170,10 @@
      */
     @Test
     public void testUnregistersEventReceivers() {
+        mCarLocationService.init();
         mCarLocationService.release();
         verify(mMockContext).unregisterReceiver(mCarLocationService);
+        verify(mMockCarDrivingStateService).unregisterDrivingStateChangeListener(any());
     }
 
     /**
@@ -177,7 +190,6 @@
         ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class);
         when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
                 .thenReturn(mMockLocationManager);
-        when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
         when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
         when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(true);
 
@@ -304,7 +316,6 @@
         timbuktu.setAccuracy(13.75f);
         timbuktu.setTime(currentTime);
         timbuktu.setElapsedRealtimeNanos(elapsedTime);
-        when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
         when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
                 .thenReturn(mMockLocationManager);
         when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
@@ -326,7 +337,7 @@
      * Test that the {@link CarLocationService} does not throw an exception on SUSPEND_EXIT events.
      */
     @Test
-    public void testDoesNotThrowExceptionUponStateChanged() {
+    public void testDoesNotThrowExceptionUponPowerStateChanged() {
         try {
             mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_ENTER, null);
             mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, null);
@@ -361,12 +372,16 @@
      */
     @Test
     public void testDeletesCacheFileWhenLocationIsDisabled() throws Exception {
+        writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
         when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
                 .thenReturn(mMockLocationManager);
         when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
         mCarLocationService.init();
+        assertTrue(getLocationCacheFile().exists());
+
         mCarLocationService.onReceive(mMockContext,
                 new Intent(LocationManager.MODE_CHANGED_ACTION));
+
         verify(mMockLocationManager, times(1)).isLocationEnabled();
         assertFalse(getLocationCacheFile().exists());
     }
@@ -394,18 +409,49 @@
      */
     @Test
     public void testDeletesCacheFileWhenTheGPSProviderIsDisabled() throws Exception {
+        writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
         when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
                 .thenReturn(mMockLocationManager);
         when(mMockLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)).thenReturn(
                 false);
         mCarLocationService.init();
+        assertTrue(getLocationCacheFile().exists());
+
         mCarLocationService.onReceive(mMockContext,
                 new Intent(LocationManager.PROVIDERS_CHANGED_ACTION));
+
         verify(mMockLocationManager, times(1))
                 .isProviderEnabled(LocationManager.GPS_PROVIDER);
         assertFalse(getLocationCacheFile().exists());
     }
 
+    /**
+     * Test that the {@link CarLocationService} deletes location_cache.json when the car enters a
+     * moving driving state.
+     */
+    @Test
+    public void testDeletesCacheFileWhenDrivingStateBecomesMoving() throws Exception {
+        writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
+        when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
+                .thenReturn(mMockLocationManager);
+        when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
+        mCarLocationService.init();
+        ArgumentCaptor<ICarDrivingStateChangeListener> changeListenerArgument =
+                ArgumentCaptor.forClass(ICarDrivingStateChangeListener.class);
+        verify(mMockCarDrivingStateService).registerDrivingStateChangeListener(
+                changeListenerArgument.capture());
+        ICarDrivingStateChangeListener changeListener = changeListenerArgument.getValue();
+        assertTrue(getLocationCacheFile().exists());
+
+        changeListener.onDrivingStateChanged(
+                new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_MOVING,
+                        SystemClock.elapsedRealtimeNanos()));
+
+        verify(mMockLocationManager, times(0)).isLocationEnabled();
+        verify(mMockCarDrivingStateService, times(1)).unregisterDrivingStateChangeListener(any());
+        assertFalse(getLocationCacheFile().exists());
+    }
+
     private void writeCacheFile(String json) throws IOException {
         FileOutputStream fos = new FileOutputStream(getLocationCacheFile());
         fos.write(json.getBytes());
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 6e0c2fe..2c63ef5 100644
--- a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
@@ -16,6 +16,8 @@
 
 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;
@@ -52,12 +54,16 @@
 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 {
@@ -66,8 +72,14 @@
     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};
 
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -84,6 +96,10 @@
     private IVmsSubscriberClient mSubscriberClient;
     @Mock
     private IVmsSubscriberClient mSubscriberClient2;
+    @Mock
+    private IVmsSubscriberClient mThrowingSubscriberClient;
+    @Mock
+    private IVmsSubscriberClient mThrowingSubscriberClient2;
 
     private VmsPublisherService mPublisherService;
     private MockPublisherClient mPublisherClient;
@@ -96,6 +112,7 @@
         mPublisherClient2 = new MockPublisherClient();
         when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
                 .thenReturn(new HashSet<>(Arrays.asList(mSubscriberClient, mSubscriberClient2)));
+
     }
 
     @Test
@@ -175,6 +192,15 @@
     }
 
     @Test
+    public void testPublishNullLayerAndNullPayload() throws Exception {
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
+
+        // We just want to ensure that no exceptions are thrown here.
+        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, null, PUBLISHER_ID,
+                null);
+    }
+
+    @Test
     public void testPublish_ClientError() throws Exception {
         mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
         doThrow(new RemoteException()).when(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
@@ -274,6 +300,341 @@
     }
 
     @Test
+    public void testDump_getPacketCount() throws Exception {
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
+        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.asBinder());
+        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.asBinder());
+        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.asBinder());
+        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2.asBinder());
+
+        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(mBrokerService.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
+
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
+
+        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(mBrokerService.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
+        when(mBrokerService.getPackageName(mThrowingSubscriberClient2)).thenReturn("Thrower2");
+
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
+        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2.asBinder());
+
+        // 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(mBrokerService.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
+
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient.asBinder());
+        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();
         verify(mClientManager).unregisterConnectionListener(mPublisherService);