Merge "Create a new car service lib just for the test lib"
diff --git a/car-lib/src/android/car/app/CarActivityView.java b/car-lib/src/android/car/app/CarActivityView.java
new file mode 100644
index 0000000..efefbe1
--- /dev/null
+++ b/car-lib/src/android/car/app/CarActivityView.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.app;
+
+import android.annotation.Nullable;
+import android.app.ActivityView;
+import android.car.Car;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+
+/**
+ * CarActivityView is a special kind of ActivityView that can track which display the ActivityView
+ * is placed.  This information can be used to enforce the driving safety.
+ *
+ * @hide
+ */
+public final class CarActivityView extends ActivityView {
+
+    private static final String TAG = CarActivityView.class.getSimpleName();
+
+    // volatile, since mUserActivityViewCallback can be accessed from Main and Binder thread.
+    @Nullable private volatile StateCallback mUserActivityViewCallback;
+
+    @Nullable private CarUxRestrictionsManager mUxRestrictionsManager;
+
+    private int mVirtualDisplayId = Display.INVALID_DISPLAY;
+
+    public CarActivityView(Context context) {
+        this(context, /*attrs=*/ null);
+    }
+
+    public CarActivityView(Context context, AttributeSet attrs) {
+        this(context, attrs, /*defStyle=*/ 0);
+    }
+
+    public CarActivityView(Context context, AttributeSet attrs, int defStyle) {
+        this(context, attrs, defStyle,  /*singleTaskInstance=*/ false);
+    }
+
+    public CarActivityView(
+            Context context, AttributeSet attrs, int defStyle, boolean singleTaskInstance) {
+        super(context, attrs, defStyle, singleTaskInstance);
+        super.setCallback(new CarActivityViewCallback());
+        Car.createCar(mContext, /*handler=*/ null,
+                Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT,
+                (car, ready) -> {
+                    // Expect to be called in the main thread, since passed a 'null' handler
+                    // in Car.createCar().
+                    if (!ready) return;
+                    mUxRestrictionsManager = (CarUxRestrictionsManager) car.getCarManager(
+                            Car.CAR_UX_RESTRICTION_SERVICE);
+                    if (mVirtualDisplayId != Display.INVALID_DISPLAY) {
+                        // When the CarService is reconnected, we'd like to report the physical
+                        // display id again, since the previously reported mapping could be gone.
+                        reportPhysicalDisplayId(
+                                mUxRestrictionsManager, mVirtualDisplayId, mContext.getDisplayId());
+                    }
+                });
+    }
+
+    @Override
+    public void setCallback(StateCallback callback) {
+        mUserActivityViewCallback = callback;
+        if (getVirtualDisplayId() != Display.INVALID_DISPLAY && callback != null) {
+            callback.onActivityViewReady(this);
+        }
+    }
+
+    private static void reportPhysicalDisplayId(CarUxRestrictionsManager manager,
+            int virtualDisplayId, int physicalDisplayId) {
+        Log.d(TAG, "reportPhysicalDisplayId: virtualDisplayId=" + virtualDisplayId
+                + ", physicalDisplayId=" + physicalDisplayId);
+        if (virtualDisplayId == Display.INVALID_DISPLAY) {
+            throw new RuntimeException("Has no virtual display to report.");
+        }
+        if (manager == null) {
+            Log.w(TAG, "CarUxRestrictionsManager is not ready yet");
+            return;
+        }
+        manager.reportVirtualDisplayToPhysicalDisplay(virtualDisplayId, physicalDisplayId);
+    }
+
+    // Intercepts ActivityViewCallback and reports it's display-id changes.
+    private class CarActivityViewCallback extends StateCallback {
+        // onActivityViewReady() and onActivityViewDestroyed() are called in the main thread.
+        @Override
+        public void onActivityViewReady(ActivityView activityView) {
+            // Stores the virtual display id to use it onActivityViewDestroyed().
+            mVirtualDisplayId = getVirtualDisplayId();
+            reportPhysicalDisplayId(
+                    mUxRestrictionsManager, mVirtualDisplayId, mContext.getDisplayId());
+
+            StateCallback stateCallback = mUserActivityViewCallback;
+            if (stateCallback != null) {
+                stateCallback.onActivityViewReady(activityView);
+            }
+        }
+
+        @Override
+        public void onActivityViewDestroyed(ActivityView activityView) {
+            // getVirtualDisplayId() will return INVALID_DISPLAY inside onActivityViewDestroyed(),
+            // because AV.mVirtualDisplay was already released during AV.performRelease().
+            int virtualDisplayId = mVirtualDisplayId;
+            mVirtualDisplayId = Display.INVALID_DISPLAY;
+            reportPhysicalDisplayId(
+                    mUxRestrictionsManager, virtualDisplayId, Display.INVALID_DISPLAY);
+
+            StateCallback stateCallback = mUserActivityViewCallback;
+            if (stateCallback != null) {
+                stateCallback.onActivityViewDestroyed(activityView);
+            }
+        }
+
+        @Override
+        public void onTaskCreated(int taskId, ComponentName componentName) {
+            StateCallback stateCallback = mUserActivityViewCallback;
+            if (stateCallback != null) {
+                stateCallback.onTaskCreated(taskId, componentName);
+            }
+        }
+
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            StateCallback stateCallback = mUserActivityViewCallback;
+            if (stateCallback != null) {
+                stateCallback.onTaskMovedToFront(taskId);
+            }
+        }
+
+        @Override
+        public void onTaskRemovalStarted(int taskId) {
+            StateCallback stateCallback = mUserActivityViewCallback;
+            if (stateCallback != null) {
+                stateCallback.onTaskRemovalStarted(taskId);
+            }
+        }
+    }
+}
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index be194b8..9533322 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -22,8 +22,10 @@
 import android.annotation.RequiresPermission;
 import android.car.Car;
 import android.car.CarManagerBase;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
@@ -408,4 +410,44 @@
 
         return mDisplayId;
     }
+
+    // Dummy Callback to identify the requester of reportVirtualDisplayToPhysicalDisplay() and
+    // to clean up the internal data when the requester is crashed.
+    private final IRemoteCallback mRequester = new IRemoteCallback.Stub() {
+        @Override public void sendResult(Bundle data) {}  // Unused
+    };
+
+    /**
+     * Reports the mapping the virtual display to the physical display.
+     *
+     * @param virtualDisplayId the display id of the embedded virtual display.
+     * @parom physicalDisplayId the display id where the ActivityView is placed in.
+     * @hide
+     */
+    public void reportVirtualDisplayToPhysicalDisplay(int virtualDisplayId, int physicalDisplayId) {
+        try {
+            mUxRService.reportVirtualDisplayToPhysicalDisplay(mRequester,
+                    virtualDisplayId, physicalDisplayId);
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
+
+    /**
+     * Finds out the physical display id where ActivityView is actually located in.
+     * If the given ActivityView is placed inside of another ActivityView, then it will return
+     * the display id where the parent ActivityView is located in.
+     *
+     * @param displayId the display id of the embedded virtual display of ActivityView.
+     * @return the physical display id where ActivityView is actually located in.
+     * @hide
+     */
+    public int getMappedPhysicalDisplayOfVirtualDisplay(int displayId) {
+        try {
+            return mUxRService.getMappedPhysicalDisplayOfVirtualDisplay(displayId);
+        } catch (RemoteException e) {
+            // When CarService isn't ready, we'll return DEFAULT_DISPLAY defensively.
+            return handleRemoteExceptionFromCarService(e, Display.DEFAULT_DISPLAY);
+        }
+    }
 }
diff --git a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
index 7f3a6e9..64daa57 100644
--- a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
+++ b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
@@ -19,6 +19,7 @@
 import android.car.drivingstate.CarUxRestrictions;
 import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.ICarUxRestrictionsChangeListener;
+import android.os.IRemoteCallback;
 
 /**
  * Binder interface for {@link android.car.drivingstate.CarUxRestrictionsManager}.
@@ -36,4 +37,6 @@
     List<CarUxRestrictionsConfiguration> getConfigs() = 5;
     boolean setRestrictionMode(int mode) = 6;
     int getRestrictionMode() = 7;
+    void reportVirtualDisplayToPhysicalDisplay(in IRemoteCallback binder, int virtualDisplayId, int physicalDisplayId) = 8;
+    int getMappedPhysicalDisplayOfVirtualDisplay(int displayId) = 9;
 }
diff --git a/service/src/com/android/car/CarServiceBase.java b/service/src/com/android/car/CarServiceBase.java
index e014cf0..c4b578d 100644
--- a/service/src/com/android/car/CarServiceBase.java
+++ b/service/src/com/android/car/CarServiceBase.java
@@ -36,7 +36,4 @@
     default void vehicleHalReconnected() {}
 
     void dump(PrintWriter writer);
-
-    /** Called when we only want to dump metrics instead of everything else. */
-    default void dumpMetrics(PrintWriter writer) {};
 }
diff --git a/service/src/com/android/car/CarUxRestrictionsManagerService.java b/service/src/com/android/car/CarUxRestrictionsManagerService.java
index bb49ff2..c7de564 100644
--- a/service/src/com/android/car/CarUxRestrictionsManagerService.java
+++ b/service/src/com/android/car/CarUxRestrictionsManagerService.java
@@ -45,7 +45,9 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Process;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.ArraySet;
@@ -54,6 +56,7 @@
 import android.util.JsonWriter;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayAddress;
 
@@ -303,6 +306,9 @@
         mUxRClients.clear();
         mDrivingStateService.unregisterDrivingStateChangeListener(
                 mICarDrivingStateChangeEventListener);
+        synchronized (mMapLock) {
+            mActivityViewDisplayInfoMap.clear();
+        }
     }
 
     // Binder methods
@@ -626,6 +632,12 @@
         for (Utils.TransitionLog tlog : mTransitionLogs) {
             writer.println(tlog);
         }
+        writer.println("UX Restriction display info:");
+        for (int i = mActivityViewDisplayInfoMap.size() - 1; i >= 0; --i) {
+            DisplayInfo info = mActivityViewDisplayInfoMap.valueAt(i);
+            writer.printf("Display%d: physicalDisplayId=%d, owner=%s",
+                    mActivityViewDisplayInfoMap.keyAt(i), info.mPhysicalDisplayId, info.mOwner);
+        }
     }
 
     /**
@@ -969,4 +981,93 @@
             Slog.d(TAG, msg);
         }
     }
+
+    private final Object mMapLock = new Object();
+
+    private static final class DisplayInfo {
+        final IRemoteCallback mOwner;
+        final int mPhysicalDisplayId;
+        DisplayInfo(IRemoteCallback owner, int physicalDisplayId) {
+            mOwner = owner;
+            mPhysicalDisplayId = physicalDisplayId;
+        }
+    };
+
+    @GuardedBy("mMapLock")
+    private final SparseArray<DisplayInfo> mActivityViewDisplayInfoMap = new SparseArray<>();
+
+    @GuardedBy("mMapLock")
+    private final RemoteCallbackList<IRemoteCallback> mRemoteCallbackList =
+            new RemoteCallbackList<>() {
+                @Override
+                public void onCallbackDied(IRemoteCallback callback) {
+                    synchronized (mMapLock) {
+                        // Descending order to delete items safely from SpareArray.gc().
+                        for (int i = mActivityViewDisplayInfoMap.size() - 1; i >= 0; --i) {
+                            DisplayInfo info = mActivityViewDisplayInfoMap.valueAt(i);
+                            if (info.mOwner == callback) {
+                                logd("onCallbackDied: clean up callback=" + callback);
+                                mActivityViewDisplayInfoMap.removeAt(i);
+                            }
+                        }
+                    }
+                }
+            };
+
+    @Override
+    public void reportVirtualDisplayToPhysicalDisplay(IRemoteCallback callback,
+            int virtualDisplayId, int physicalDisplayId) {
+        logd("reportVirtualDisplayToPhysicalDisplay: callback=" + callback
+                + ", virtualDisplayId=" + virtualDisplayId
+                + ", physicalDisplayId=" + physicalDisplayId);
+        boolean release = physicalDisplayId == Display.INVALID_DISPLAY;
+        checkCallerOwnsDisplay(virtualDisplayId, release);
+        synchronized (mMapLock) {
+            if (release) {
+                mRemoteCallbackList.unregister(callback);
+                mActivityViewDisplayInfoMap.delete(virtualDisplayId);
+                return;
+            }
+            mRemoteCallbackList.register(callback);
+            mActivityViewDisplayInfoMap.put(virtualDisplayId,
+                    new DisplayInfo(callback, physicalDisplayId));
+        }
+    }
+
+    @Override
+    public int getMappedPhysicalDisplayOfVirtualDisplay(int displayId) {
+        logd("getMappedPhysicalDisplayOfVirtualDisplay: displayId=" + displayId);
+        synchronized (mMapLock) {
+            DisplayInfo foundInfo = mActivityViewDisplayInfoMap.get(displayId);
+            if (foundInfo == null) {
+                return Display.INVALID_DISPLAY;
+            }
+            // ActivityView can be placed in another ActivityView, so we should repeat the process
+            // until no parent is found (reached to the physical display).
+            while (foundInfo != null) {
+                displayId = foundInfo.mPhysicalDisplayId;
+                foundInfo = mActivityViewDisplayInfoMap.get(displayId);
+            }
+        }
+        return displayId;
+    }
+
+    private void checkCallerOwnsDisplay(int displayId, boolean release) {
+        Display display = mDisplayManager.getDisplay(displayId);
+        if (display == null) {
+            // Bypasses the permission check for non-existing display when releasing it, since
+            // reportVirtualDisplayToPhysicalDisplay() and releasing display happens simultaneously
+            // and it's no harm to release the information on the non-existing display.
+            if (release) return;
+            throw new IllegalArgumentException(
+                    "Cannot find display for non-existent displayId: " + displayId);
+        }
+
+        int callingUid = Binder.getCallingUid();
+        int displayOwnerUid = display.getOwnerUid();
+        if (callingUid != displayOwnerUid) {
+            throw new SecurityException("The caller doesn't own the display: callingUid="
+                    + callingUid + ", displayOwnerUid=" + displayOwnerUid);
+        }
+    }
 }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index dfdee04..2de5d5c 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -19,12 +19,15 @@
 import android.annotation.MainThread;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.UiModeManager;
 import android.car.Car;
 import android.car.ICar;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.hardware.automotive.vehicle.V2_0.IVehicle;
@@ -50,6 +53,7 @@
 import com.android.car.hal.VehicleHal;
 import com.android.car.internal.FeatureConfiguration;
 import com.android.car.pm.CarPackageManagerService;
+import com.android.car.stats.CarStatsService;
 import com.android.car.systeminterface.SystemInterface;
 import com.android.car.trust.CarTrustedDeviceService;
 import com.android.car.user.CarUserNoticeService;
@@ -110,6 +114,7 @@
     private final VmsSubscriberService mVmsSubscriberService;
     private final VmsPublisherService mVmsPublisherService;
     private final CarBugreportManagerService mCarBugreportManagerService;
+    private final CarStatsService mCarStatsService;
 
     private final CarServiceBase[] mAllServices;
 
@@ -167,13 +172,16 @@
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
                 serviceContext, mCarAudioService, this);
+        mCarStatsService = new CarStatsService(serviceContext);
         mVmsBrokerService = new VmsBrokerService();
         mVmsClientManager = new VmsClientManager(
-                serviceContext, mVmsBrokerService, mCarUserService, mHal.getVmsHal());
+                // CarStatsService needs to be passed to the constructor due to HAL init order
+                serviceContext, mCarStatsService, mCarUserService, mVmsBrokerService,
+                mHal.getVmsHal());
         mVmsSubscriberService = new VmsSubscriberService(
                 serviceContext, mVmsBrokerService, mVmsClientManager, mHal.getVmsHal());
         mVmsPublisherService = new VmsPublisherService(
-                serviceContext, mVmsBrokerService, mVmsClientManager);
+                serviceContext, mCarStatsService, mVmsBrokerService, mVmsClientManager);
         mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
         mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
                 systemInterface);
@@ -486,7 +494,7 @@
             writer.println("*FutureConfig, DEFAULT:" + FeatureConfiguration.DEFAULT);
             writer.println("*Dump all services*");
 
-            dumpAllServices(writer, false);
+            dumpAllServices(writer);
 
             writer.println("*Dump Vehicle HAL*");
             writer.println("Vehicle HAL Interface: " + mVehicleInterfaceName);
@@ -511,8 +519,8 @@
             dumpIndividualServices(writer, services);
             return;
         } else if ("--metrics".equals(args[0])) {
-            writer.println("*Dump car service metrics*");
-            dumpAllServices(writer, true);
+            // Strip the --metrics flag when passing dumpsys arguments to CarStatsService
+            mCarStatsService.dump(fd, writer, Arrays.copyOfRange(args, 1, args.length));
         } else if ("--vms-hal".equals(args[0])) {
             mHal.getVmsHal().dumpMetrics(fd);
         } else if (Build.IS_USERDEBUG || Build.IS_ENG) {
@@ -535,12 +543,12 @@
         }
     }
 
-    private void dumpAllServices(PrintWriter writer, boolean dumpMetricsOnly) {
+    private void dumpAllServices(PrintWriter writer) {
         for (CarServiceBase service : mAllServices) {
-            dumpService(service, writer, dumpMetricsOnly);
+            dumpService(service, writer);
         }
         if (mCarTestService != null) {
-            dumpService(mCarTestService, writer, dumpMetricsOnly);
+            dumpService(mCarTestService, writer);
         }
     }
 
@@ -551,7 +559,7 @@
             if (service == null) {
                 writer.println("No such service!");
             } else {
-                dumpService(service, writer, /* dumpMetricsOnly= */ false);
+                dumpService(service, writer);
             }
             writer.println();
         }
@@ -564,13 +572,9 @@
                 .findFirst().orElse(null);
     }
 
-    private void dumpService(CarServiceBase service, PrintWriter writer, boolean dumpMetricsOnly) {
+    private void dumpService(CarServiceBase service, PrintWriter writer) {
         try {
-            if (dumpMetricsOnly) {
-                service.dumpMetrics(writer);
-            } else {
-                service.dump(writer);
-            }
+            service.dump(writer);
         } catch (Exception e) {
             writer.println("Failed dumping: " + service.getClass().getName());
             e.printStackTrace(writer);
@@ -609,6 +613,8 @@
         private static final String COMMAND_ENABLE_TRUSTED_DEVICE = "enable-trusted-device";
         private static final String COMMAND_REMOVE_TRUSTED_DEVICES = "remove-trusted-devices";
         private static final String COMMAND_SET_UID_TO_ZONE = "set-zoneid-for-uid";
+        private static final String COMMAND_START_FIXED_ACTIVITY_MODE = "start-fixed-activity-mode";
+        private static final String COMMAND_STOP_FIXED_ACTIVITY_MODE = "stop-fixed-activity-mode";
 
         private static final String PARAM_DAY_MODE = "day";
         private static final String PARAM_NIGHT_MODE = "night";
@@ -683,6 +689,11 @@
             pw.println("\t  When used with dumpsys, only metrics will be in the dumpsys output.");
             pw.println("\tset-zoneid-for-uid [zoneid] [uid]");
             pw.println("\t Maps the audio zoneid to uid.");
+            pw.println("\tstart-fixed-activity displayId packageName activityName");
+            pw.println("\t  Start an Activity the specified display as fixed mode");
+            pw.println("\tstop-fixed-mode displayId");
+            pw.println("\t  Stop fixed Activity mode for the given display. "
+                    + "The Activity will not be restarted upon crash.");
         }
 
         private int dumpInvalidArguments(PrintWriter pw) {
@@ -820,6 +831,12 @@
                         dumpHelp(writer);
                     }
                     break;
+                case COMMAND_START_FIXED_ACTIVITY_MODE:
+                    handleStartFixedActivity(args, writer);
+                    break;
+                case COMMAND_STOP_FIXED_ACTIVITY_MODE:
+                    handleStopFixedMode(args, writer);
+                    break;
                 default:
                     writer.println("Unknown command: \"" + arg + "\"");
                     dumpHelp(writer);
@@ -828,6 +845,50 @@
             return RESULT_OK;
         }
 
+        private void handleStartFixedActivity(String[] args, PrintWriter writer) {
+            if (args.length != 4) {
+                writer.println("Incorrect number of arguments");
+                dumpHelp(writer);
+                return;
+            }
+            int displayId;
+            try {
+                displayId = Integer.parseInt(args[1]);
+            } catch (NumberFormatException e) {
+                writer.println("Wrong display id:" + args[1]);
+                return;
+            }
+            String packageName = args[2];
+            String activityName = args[3];
+            Intent intent = new Intent();
+            intent.setComponent(new ComponentName(packageName, activityName));
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            ActivityOptions options = ActivityOptions.makeBasic();
+            options.setLaunchDisplayId(displayId);
+            if (!mFixedActivityService.startFixedActivityModeForDisplayAndUser(intent, options,
+                    displayId, ActivityManager.getCurrentUser())) {
+                writer.println("Failed to start");
+                return;
+            }
+            writer.println("Succeeded");
+        }
+
+        private void handleStopFixedMode(String[] args, PrintWriter writer) {
+            if (args.length != 2) {
+                writer.println("Incorrect number of arguments");
+                dumpHelp(writer);
+                return;
+            }
+            int displayId;
+            try {
+                displayId = Integer.parseInt(args[1]);
+            } catch (NumberFormatException e) {
+                writer.println("Wrong display id:" + args[1]);
+                return;
+            }
+            mFixedActivityService.stopFixedActivityMode(displayId);
+        }
+
         private void forceDayNightMode(String arg, PrintWriter writer) {
             int mode;
             switch (arg) {
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 3029a9e..da0b8b7 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -29,16 +29,16 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.car.stats.CarStatsService;
 import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.util.Collections;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
+import java.util.function.IntSupplier;
 
 
 /**
@@ -50,75 +50,35 @@
     private static final boolean DBG = false;
     private static final String TAG = "VmsPublisherService";
 
-    @VisibleForTesting
-    static final String PACKET_COUNT_FORMAT = "Packet count for layer %s: %d\n";
-
-    @VisibleForTesting
-    static final String PACKET_SIZE_FORMAT = "Total packet size for layer %s: %d (bytes)\n";
-
-    @VisibleForTesting
-    static final String PACKET_FAILURE_COUNT_FORMAT =
-            "Total packet failure count for layer %s from %s to %s: %d\n";
-
-    @VisibleForTesting
-    static final String PACKET_FAILURE_SIZE_FORMAT =
-            "Total packet failure size for layer %s from %s to %s: %d (bytes)\n";
-
     private final Context mContext;
+    private final CarStatsService mStatsService;
     private final VmsBrokerService mBrokerService;
     private final VmsClientManager mClientManager;
+    private final IntSupplier mGetCallingUid;
     private final Map<String, PublisherProxy> mPublisherProxies = Collections.synchronizedMap(
             new ArrayMap<>());
 
-    @GuardedBy("mPacketCounts")
-    private final Map<VmsLayer, PacketCountAndSize> mPacketCounts = new ArrayMap<>();
-    @GuardedBy("mPacketFailureCounts")
-    private final Map<PacketFailureKey, PacketCountAndSize> mPacketFailureCounts = new ArrayMap<>();
-
-    // PacketCountAndSize keeps track of the cumulative size and number of packets of a specific
-    // VmsLayer that we have seen.
-    private class PacketCountAndSize {
-        long mCount;
-        long mSize;
-    }
-
-    // PacketFailureKey is a triple of the VmsLayer, the publisher and subscriber for which a packet
-    // failed to be sent.
-    private class PacketFailureKey {
-        VmsLayer mVmsLayer;
-        String mPublisher;
-        String mSubscriber;
-
-        PacketFailureKey(VmsLayer vmsLayer, String publisher, String subscriber) {
-            mVmsLayer = vmsLayer;
-            mPublisher = publisher;
-            mSubscriber = subscriber;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (!(o instanceof PacketFailureKey)) {
-                return false;
-            }
-
-            PacketFailureKey otherKey = (PacketFailureKey) o;
-            return Objects.equals(mVmsLayer, otherKey.mVmsLayer) && Objects.equals(mPublisher,
-                    otherKey.mPublisher) && Objects.equals(mSubscriber, otherKey.mSubscriber);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mVmsLayer, mPublisher, mSubscriber);
-        }
-    }
-
-    public VmsPublisherService(
+    VmsPublisherService(
             Context context,
+            CarStatsService statsService,
             VmsBrokerService brokerService,
             VmsClientManager clientManager) {
+        this(context, statsService, brokerService, clientManager, Binder::getCallingUid);
+    }
+
+    @VisibleForTesting
+    VmsPublisherService(
+            Context context,
+            CarStatsService statsService,
+            VmsBrokerService brokerService,
+            VmsClientManager clientManager,
+            IntSupplier getCallingUid) {
         mContext = context;
+        mStatsService = statsService;
         mBrokerService = brokerService;
         mClientManager = clientManager;
+        mGetCallingUid = getCallingUid;
+
         mClientManager.setPublisherService(this);
     }
 
@@ -133,35 +93,8 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        dumpMetrics(writer);
-    }
-
-    @Override
-    public void dumpMetrics(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
         writer.println("mPublisherProxies: " + mPublisherProxies.size());
-        synchronized (mPacketCounts) {
-            for (Map.Entry<VmsLayer, PacketCountAndSize> entry : mPacketCounts.entrySet()) {
-                VmsLayer layer = entry.getKey();
-                PacketCountAndSize countAndSize = entry.getValue();
-                writer.format(PACKET_COUNT_FORMAT, layer, countAndSize.mCount);
-                writer.format(PACKET_SIZE_FORMAT, layer, countAndSize.mSize);
-            }
-        }
-        synchronized (mPacketFailureCounts) {
-            for (Map.Entry<PacketFailureKey, PacketCountAndSize> entry :
-                    mPacketFailureCounts.entrySet()) {
-                PacketFailureKey key = entry.getKey();
-                PacketCountAndSize countAndSize = entry.getValue();
-                VmsLayer layer = key.mVmsLayer;
-                String publisher = key.mPublisher;
-                String subscriber = key.mSubscriber;
-                writer.format(PACKET_FAILURE_COUNT_FORMAT, layer, publisher, subscriber,
-                        countAndSize.mCount);
-                writer.format(PACKET_FAILURE_SIZE_FORMAT, layer, publisher, subscriber,
-                        countAndSize.mSize);
-            }
-        }
     }
 
     /**
@@ -236,26 +169,6 @@
             mBrokerService.setPublisherLayersOffering(token, offering);
         }
 
-        private void incrementPacketCount(VmsLayer layer, long size) {
-            synchronized (mPacketCounts) {
-                PacketCountAndSize countAndSize = mPacketCounts.computeIfAbsent(layer,
-                        i -> new PacketCountAndSize());
-                countAndSize.mCount++;
-                countAndSize.mSize += size;
-            }
-        }
-
-        private void incrementPacketFailure(VmsLayer layer, String publisher, String subscriber,
-                long size) {
-            synchronized (mPacketFailureCounts) {
-                PacketFailureKey key = new PacketFailureKey(layer, publisher, subscriber);
-                PacketCountAndSize countAndSize = mPacketFailureCounts.computeIfAbsent(key,
-                        i -> new PacketCountAndSize());
-                countAndSize.mCount++;
-                countAndSize.mSize += size;
-            }
-        }
-
         @Override
         public void publish(IBinder token, VmsLayer layer, int publisherId, byte[] payload) {
             assertPermission(token);
@@ -268,7 +181,8 @@
             }
 
             int payloadLength = payload != null ? payload.length : 0;
-            incrementPacketCount(layer, payloadLength);
+            mStatsService.getVmsClientLog(mGetCallingUid.getAsInt())
+                    .logPacketSent(layer, payloadLength);
 
             // Send the message to subscribers
             Set<IVmsSubscriberClient> listeners =
@@ -277,17 +191,21 @@
             if (DBG) Log.d(TAG, String.format("Number of subscribers: %d", listeners.size()));
 
             if (listeners.size() == 0) {
-                // An empty string for the last argument is a special value signalizing zero
-                // subscribers for the VMS_PACKET_FAILURE_REPORTED atom.
-                incrementPacketFailure(layer, mName, "", payloadLength);
+                // A negative UID signals that the packet had zero subscribers
+                mStatsService.getVmsClientLog(-1)
+                        .logPacketDropped(layer, payloadLength);
             }
 
             for (IVmsSubscriberClient listener : listeners) {
+                int subscriberUid = mClientManager.getSubscriberUid(listener);
                 try {
                     listener.onVmsMessageReceived(layer, payload);
+                    mStatsService.getVmsClientLog(subscriberUid)
+                            .logPacketReceived(layer, payloadLength);
                 } catch (RemoteException ex) {
+                    mStatsService.getVmsClientLog(subscriberUid)
+                            .logPacketDropped(layer, payloadLength);
                     String subscriberName = mClientManager.getPackageName(listener);
-                    incrementPacketFailure(layer, mName, subscriberName, payloadLength);
                     Log.e(TAG, String.format("Unable to publish to listener: %s", subscriberName));
                 }
             }
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
index b8f8608..20885b8 100644
--- a/service/src/com/android/car/am/FixedActivityService.java
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -15,6 +15,9 @@
  */
 package com.android.car.am;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.Process.INVALID_UID;
+
 import static com.android.car.CarLog.TAG_AM;
 
 import android.annotation.NonNull;
@@ -26,6 +29,7 @@
 import android.app.IActivityManager;
 import android.app.IProcessObserver;
 import android.app.TaskStackListener;
+import android.car.hardware.power.CarPowerManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -34,7 +38,10 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.HandlerThread;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
@@ -60,6 +67,11 @@
 
     private static final boolean DBG = false;
 
+    private static final long RECHECK_INTERVAL_MS = 500;
+    private static final int MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY = 5;
+    // If process keep running without crashing, will reset consecutive crash counts.
+    private static final long CRASH_FORGET_INTERVAL_MS = 2 * 60 * 1000; // 2 mins
+
     private static class RunningActivityInfo {
         @NonNull
         public final Intent intent;
@@ -70,9 +82,20 @@
         @UserIdInt
         public final int userId;
 
-        // Only used in a method for local book-keeping. So do not need a lock.
-        // This does not represent the current visibility.
+        @GuardedBy("mLock")
         public boolean isVisible;
+        @GuardedBy("mLock")
+        public long lastLaunchTimeMs = 0;
+        @GuardedBy("mLock")
+        public int consecutiveRetries = 0;
+        @GuardedBy("mLock")
+        public int taskId = INVALID_TASK_ID;
+        @GuardedBy("mLock")
+        public int previousTaskId = INVALID_TASK_ID;
+        @GuardedBy("mLock")
+        public boolean inBackground;
+        @GuardedBy("mLock")
+        public boolean failureLogged;
 
         RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
                 @UserIdInt int userId) {
@@ -81,10 +104,17 @@
             this.userId = userId;
         }
 
+        private void resetCrashCounterLocked() {
+            consecutiveRetries = 0;
+            failureLogged = false;
+        }
+
         @Override
         public String toString() {
             return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
-                    + ",userId:" + userId + "}";
+                    + ",userId:" + userId + ",isVisible:" + isVisible
+                    + ",lastLaunchTimeMs:" + lastLaunchTimeMs
+                    + ",consecutiveRetries:" + consecutiveRetries + ",taskId:" + taskId + "}";
         }
     }
 
@@ -111,11 +141,41 @@
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
+            String action = intent.getAction();
             if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
                     || Intent.ACTION_PACKAGE_REPLACED.equals(
                     action)) {
-                launchIfNecessary();
+                Uri packageData = intent.getData();
+                if (packageData == null) {
+                    Log.w(TAG_AM, "null packageData");
+                    return;
+                }
+                String packageName = packageData.getSchemeSpecificPart();
+                if (packageName == null) {
+                    Log.w(TAG_AM, "null packageName");
+                    return;
+                }
+                int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                int userId = UserHandle.getUserId(uid);
+                boolean tryLaunch = false;
+                synchronized (mLock) {
+                    for (int i = 0; i < mRunningActivities.size(); i++) {
+                        RunningActivityInfo info = mRunningActivities.valueAt(i);
+                        ComponentName component = info.intent.getComponent();
+                        // should do this for all activities as the same package can cover multiple
+                        // displays.
+                        if (packageName.equals(component.getPackageName())
+                                && info.userId == userId) {
+                            Log.i(TAG_AM, "Package updated:" + packageName
+                                    + ",user:" + userId);
+                            info.resetCrashCounterLocked();
+                            tryLaunch = true;
+                        }
+                    }
+                }
+                if (tryLaunch) {
+                    launchIfNecessary();
+                }
             }
         }
     };
@@ -126,6 +186,26 @@
         public void onTaskStackChanged() {
             launchIfNecessary();
         }
+
+        @Override
+        public void onTaskCreated(int taskId, ComponentName componentName) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskRemoved(int taskId) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onTaskRemovalStarted(int taskId) {
+            launchIfNecessary();
+        }
     };
 
     private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@@ -145,6 +225,13 @@
         }
     };
 
+    private final HandlerThread mHandlerThread = new HandlerThread(
+            FixedActivityService.class.getSimpleName());
+
+    private final Runnable mActivityCheckRunnable = () -> {
+        launchIfNecessary();
+    };
+
     private final Object mLock = new Object();
 
     // key: displayId
@@ -155,13 +242,29 @@
     @GuardedBy("mLock")
     private boolean mEventMonitoringActive;
 
+    @GuardedBy("mLock")
+    private CarPowerManager mCarPowerManager;
+
+    private final CarPowerManager.CarPowerStateListener mCarPowerStateListener = (state) -> {
+        if (state != CarPowerManager.CarPowerStateListener.ON) {
+            return;
+        }
+        synchronized (mLock) {
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                RunningActivityInfo info = mRunningActivities.valueAt(i);
+                info.resetCrashCounterLocked();
+            }
+        }
+        launchIfNecessary();
+    };
+
     public FixedActivityService(Context context) {
         mContext = context;
         mAm = ActivityManager.getService();
         mUm = context.getSystemService(UserManager.class);
+        mHandlerThread.start();
     }
 
-
     @Override
     public void init() {
         // nothing to do
@@ -181,18 +284,26 @@
         }
     }
 
+    private void postRecheck(long delayMs) {
+        mHandlerThread.getThreadHandler().postDelayed(mActivityCheckRunnable, delayMs);
+    }
+
     private void startMonitoringEvents() {
+        CarPowerManager carPowerManager;
         synchronized (mLock) {
             if (mEventMonitoringActive) {
                 return;
             }
             mEventMonitoringActive = true;
+            carPowerManager = CarLocalServices.createCarPowerManager(mContext);
+            mCarPowerManager = carPowerManager;
         }
         CarUserService userService = CarLocalServices.getService(CarUserService.class);
         userService.addUserCallback(mUserCallback);
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        filter.addDataScheme("package");
         mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
                 /* broadcastPermission= */ null, /* scheduler= */ null);
         try {
@@ -201,15 +312,28 @@
         } catch (RemoteException e) {
             Log.e(TAG_AM, "remote exception from AM", e);
         }
+        try {
+            carPowerManager.setListener(mCarPowerStateListener);
+        } catch (Exception e) {
+            // should not happen
+            Log.e(TAG_AM, "Got exception from CarPowerManager", e);
+        }
     }
 
     private void stopMonitoringEvents() {
+        CarPowerManager carPowerManager;
         synchronized (mLock) {
             if (!mEventMonitoringActive) {
                 return;
             }
             mEventMonitoringActive = false;
+            carPowerManager = mCarPowerManager;
+            mCarPowerManager = null;
         }
+        if (carPowerManager != null) {
+            carPowerManager.clearListener();
+        }
+        mHandlerThread.getThreadHandler().removeCallbacks(mActivityCheckRunnable);
         CarUserService userService = CarLocalServices.getService(CarUserService.class);
         userService.removeUserCallback(mUserCallback);
         try {
@@ -243,6 +367,7 @@
             Log.e(TAG_AM, "cannot get StackInfo from AM");
             return false;
         }
+        long now = SystemClock.elapsedRealtime();
         synchronized (mLock) {
             if (mRunningActivities.size() == 0) {
                 // it must have been stopped.
@@ -264,16 +389,30 @@
                         && activityInfo.userId == topUserId && stackInfo.visible) {
                     // top one is matching.
                     activityInfo.isVisible = true;
+                    activityInfo.taskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
                     continue;
                 }
-                if (DBG) {
-                    Log.i(TAG_AM, "Unmatched top activity:" + stackInfo.topActivity
-                            + " user:" + topUserId + " display:" + stackInfo.displayId);
+                activityInfo.previousTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+                Log.i(TAG_AM, "Unmatched top activity will be removed:"
+                        + stackInfo.topActivity + " top task id:" + activityInfo.previousTaskId
+                        + " user:" + topUserId + " display:" + stackInfo.displayId);
+                activityInfo.inBackground = false;
+                for (int i = 0; i < stackInfo.taskIds.length - 1; i++) {
+                    if (activityInfo.taskId == stackInfo.taskIds[i]) {
+                        activityInfo.inBackground = true;
+                    }
+                }
+                if (!activityInfo.inBackground) {
+                    activityInfo.taskId = INVALID_TASK_ID;
                 }
             }
             for (int i = 0; i < mRunningActivities.size(); i++) {
                 RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
+                long timeSinceLastLaunchMs = now - activityInfo.lastLaunchTimeMs;
                 if (activityInfo.isVisible) {
+                    if (timeSinceLastLaunchMs >= CRASH_FORGET_INTERVAL_MS) {
+                        activityInfo.consecutiveRetries = 0;
+                    }
                     continue;
                 }
                 if (!isComponentAvailable(activityInfo.intent.getComponent(),
@@ -281,14 +420,40 @@
                         activityInfo.userId)) {
                     continue;
                 }
+                // For 1st call (consecutiveRetries == 0), do not wait as there can be no posting
+                // for recheck.
+                if (activityInfo.consecutiveRetries > 0 && (timeSinceLastLaunchMs
+                        < RECHECK_INTERVAL_MS)) {
+                    // wait until next check interval comes.
+                    continue;
+                }
+                if (activityInfo.consecutiveRetries >= MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY) {
+                    // re-tried too many times, give up for now.
+                    if (!activityInfo.failureLogged) {
+                        activityInfo.failureLogged = true;
+                        Log.w(TAG_AM, "Too many relaunch failure of fixed activity:"
+                                + activityInfo);
+                    }
+                    continue;
+                }
+
                 Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
                         + ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
                         + mRunningActivities.keyAt(i));
+                // Increase retry count if task is not in background. In case like other app is
+                // launched and the target activity is still in background, do not consider it
+                // as retry.
+                if (!activityInfo.inBackground) {
+                    activityInfo.consecutiveRetries++;
+                }
                 try {
+                    postRecheck(RECHECK_INTERVAL_MS);
+                    postRecheck(CRASH_FORGET_INTERVAL_MS);
                     mContext.startActivityAsUser(activityInfo.intent,
                             activityInfo.activityOptions.toBundle(),
                             UserHandle.of(activityInfo.userId));
                     activityInfo.isVisible = true;
+                    activityInfo.lastLaunchTimeMs = SystemClock.elapsedRealtime();
                 } catch (Exception e) { // Catch all for any app related issues.
                     Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
                 }
@@ -368,6 +533,10 @@
         if (!isDisplayAllowedForFixedMode(displayId)) {
             return false;
         }
+        if (options == null) {
+            Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, null options");
+            return false;
+        }
         if (!isUserAllowedToLaunchActivity(userId)) {
             Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
                     + " cannot launch activity, Intent:" + intent);
@@ -390,7 +559,16 @@
                 startMonitoringEvents = true;
             }
             RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
-            if (activityInfo == null) {
+            boolean replaceEntry = true;
+            if (activityInfo != null && activityInfo.intent.equals(intent)
+                    && options.equals(activityInfo.activityOptions)
+                    && userId == activityInfo.userId) {
+                replaceEntry = false;
+                if (activityInfo.isVisible) { // already shown.
+                    return true;
+                }
+            }
+            if (replaceEntry) {
                 activityInfo = new RunningActivityInfo(intent, options, userId);
                 mRunningActivities.put(displayId, activityInfo);
             }
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index 6e186d5..6be45b5 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -135,6 +136,7 @@
                 @Override
                 public boolean startFixedActivityModeForDisplayAndUser(Intent intent,
                         Bundle activityOptionsBundle, int userId) {
+                    Binder.clearCallingIdentity();
                     ActivityOptions options = new ActivityOptions(activityOptionsBundle);
                     FixedActivityService service = CarLocalServices.getService(
                             FixedActivityService.class);
@@ -144,6 +146,7 @@
 
                 @Override
                 public void stopFixedActivityMode(int displayId) {
+                    Binder.clearCallingIdentity();
                     FixedActivityService service = CarLocalServices.getService(
                             FixedActivityService.class);
                     service.stopFixedActivityMode(displayId);
diff --git a/service/src/com/android/car/stats/CarStatsService.java b/service/src/com/android/car/stats/CarStatsService.java
new file mode 100644
index 0000000..db23355
--- /dev/null
+++ b/service/src/com/android/car/stats/CarStatsService.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.stats;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.car.stats.VmsClientLog.ConnectionState;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Implements collection and dumpsys reporting of statistics in CSV format.
+ */
+public class CarStatsService {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "CarStatsService";
+    private static final String VMS_CONNECTION_STATS_DUMPSYS_HEADER =
+            "uid,packageName,attempts,connected,disconnected,terminated,errors";
+
+    private static final Function<VmsClientLog, String> VMS_CONNECTION_STATS_DUMPSYS_FORMAT =
+            entry -> String.format(Locale.US,
+                    "%d,%s,%d,%d,%d,%d,%d",
+                    entry.getUid(), entry.getPackageName(),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTING),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTED),
+                    entry.getConnectionStateCount(ConnectionState.DISCONNECTED),
+                    entry.getConnectionStateCount(ConnectionState.TERMINATED),
+                    entry.getConnectionStateCount(ConnectionState.CONNECTION_ERROR));
+
+    private static final String VMS_CLIENT_STATS_DUMPSYS_HEADER =
+            "uid,layerType,layerChannel,layerVersion,"
+                    + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets";
+
+    private static final Function<VmsClientStats, String> VMS_CLIENT_STATS_DUMPSYS_FORMAT =
+            entry -> String.format(
+                    "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
+                    entry.getUid(),
+                    entry.getLayerType(), entry.getLayerChannel(), entry.getLayerVersion(),
+                    entry.getTxBytes(), entry.getTxPackets(),
+                    entry.getRxBytes(), entry.getRxPackets(),
+                    entry.getDroppedBytes(), entry.getDroppedPackets());
+
+    private static final Comparator<VmsClientStats> VMS_CLIENT_STATS_ORDER =
+            Comparator.comparingInt(VmsClientStats::getUid)
+                    .thenComparingInt(VmsClientStats::getLayerType)
+                    .thenComparingInt(VmsClientStats::getLayerChannel)
+                    .thenComparingInt(VmsClientStats::getLayerVersion);
+
+    private final PackageManager mPackageManager;
+
+    @GuardedBy("mVmsClientStats")
+    private final Map<Integer, VmsClientLog> mVmsClientStats = new ArrayMap<>();
+
+    public CarStatsService(Context context) {
+        mPackageManager = context.getPackageManager();
+    }
+
+    /**
+     * Gets a logger for the VMS client with a given UID.
+     */
+    public VmsClientLog getVmsClientLog(int clientUid) {
+        synchronized (mVmsClientStats) {
+            return mVmsClientStats.computeIfAbsent(
+                    clientUid,
+                    uid -> {
+                        String packageName = mPackageManager.getNameForUid(uid);
+                        if (DEBUG) {
+                            Log.d(TAG, "Created VmsClientLog: " + packageName);
+                        }
+                        return new VmsClientLog(uid, packageName);
+                    });
+        }
+    }
+
+    /**
+     * Dumps metrics in CSV format.
+     */
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        List<String> flags = Arrays.asList(args);
+        if (args.length == 0 || flags.contains("--vms-client")) {
+            dumpVmsStats(writer);
+        }
+    }
+
+    private void dumpVmsStats(PrintWriter writer) {
+        synchronized (mVmsClientStats) {
+            writer.println(VMS_CONNECTION_STATS_DUMPSYS_HEADER);
+            mVmsClientStats.values().stream()
+                    // Unknown UID will not have connection stats
+                    .filter(entry -> entry.getUid() > 0)
+                    // Sort stats by UID
+                    .sorted(Comparator.comparingInt(VmsClientLog::getUid))
+                    .forEachOrdered(entry -> writer.println(
+                            VMS_CONNECTION_STATS_DUMPSYS_FORMAT.apply(entry)));
+            writer.println();
+
+            writer.println(VMS_CLIENT_STATS_DUMPSYS_HEADER);
+            mVmsClientStats.values().stream()
+                    .flatMap(log -> log.getLayerEntries().stream())
+                    .sorted(VMS_CLIENT_STATS_ORDER)
+                    .forEachOrdered(entry -> writer.println(
+                            VMS_CLIENT_STATS_DUMPSYS_FORMAT.apply(entry)));
+        }
+    }
+}
diff --git a/service/src/com/android/car/stats/VmsClientLog.java b/service/src/com/android/car/stats/VmsClientLog.java
new file mode 100644
index 0000000..506a5fc
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientLog.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.stats;
+
+import android.annotation.Nullable;
+import android.car.vms.VmsLayer;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * Logger for per-client VMS statistics.
+ */
+public class VmsClientLog {
+    /**
+     * Constants used for identifying client connection states.
+     */
+    public static class ConnectionState {
+        // Attempting to connect to the client
+        public static final int CONNECTING = 0;
+        // Client connection established
+        public static final int CONNECTED = 1;
+        // Client connection closed unexpectedly
+        public static final int DISCONNECTED = 2;
+        // Client connection closed by VMS
+        public static final int TERMINATED = 3;
+        // Error establishing the client connection
+        public static final int CONNECTION_ERROR = 4;
+    }
+
+    private final Object mLock = new Object();
+
+    private final int mUid;
+    private final String mPackageName;
+
+    @GuardedBy("mLock")
+    private Map<Integer, AtomicLong> mConnectionStateCounters = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    private final Map<VmsLayer, VmsClientStats> mLayerStats = new ArrayMap<>();
+
+    VmsClientLog(int clientUid, @Nullable String clientPackage) {
+        mUid = clientUid;
+        mPackageName = clientPackage != null ? clientPackage : "";
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Logs a connection state change for the client.
+     *
+     * @param connectionState New connection state
+     */
+    public void logConnectionState(int connectionState) {
+        AtomicLong counter;
+        synchronized (mLock) {
+            counter = mConnectionStateCounters.computeIfAbsent(connectionState,
+                    ignored -> new AtomicLong());
+        }
+        counter.incrementAndGet();
+    }
+
+    long getConnectionStateCount(int connectionState) {
+        AtomicLong counter;
+        synchronized (mLock) {
+            counter = mConnectionStateCounters.get(connectionState);
+        }
+        return counter == null ? 0L : counter.get();
+    }
+
+    /**
+     * Logs that a packet was published by the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketSent(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetSent(size);
+    }
+
+    /**
+     * Logs that a packet was received successfully by the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketReceived(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetReceived(size);
+    }
+
+    /**
+     * Logs that a packet was dropped due to an error delivering to the client.
+     *
+     * @param layer Layer of packet
+     * @param size Size of packet
+     */
+    public void logPacketDropped(VmsLayer layer, long size) {
+        getLayerEntry(layer).packetDropped(size);
+    }
+
+    Collection<VmsClientStats> getLayerEntries() {
+        synchronized (mLock) {
+            return mLayerStats.values().stream()
+                    .map(VmsClientStats::new) // Make a deep copy of the entries
+                    .collect(Collectors.toList());
+        }
+    }
+
+    private VmsClientStats getLayerEntry(VmsLayer layer) {
+        synchronized (mLock) {
+            return mLayerStats.computeIfAbsent(
+                    layer,
+                    (k) -> new VmsClientStats(mUid, layer));
+        }
+    }
+}
diff --git a/service/src/com/android/car/stats/VmsClientStats.java b/service/src/com/android/car/stats/VmsClientStats.java
new file mode 100644
index 0000000..f4ce7b8
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientStats.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.stats;
+
+import android.car.vms.VmsLayer;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Java representation of VmsClientStats statsd atom.
+ *
+ * All access to this class is synchronized through VmsClientLog.
+ */
+class VmsClientStats {
+    private final Object mLock = new Object();
+
+    private final int mUid;
+
+    private final int mLayerType;
+    private final int mLayerChannel;
+    private final int mLayerVersion;
+
+    @GuardedBy("mLock")
+    private long mTxBytes;
+    @GuardedBy("mLock")
+    private long mTxPackets;
+
+    @GuardedBy("mLock")
+    private long mRxBytes;
+    @GuardedBy("mLock")
+    private long mRxPackets;
+
+    @GuardedBy("mLock")
+    private long mDroppedBytes;
+    @GuardedBy("mLock")
+    private long mDroppedPackets;
+
+    /**
+     * Constructor for a VmsClientStats entry.
+     *
+     * @param uid UID of client package.
+     * @param layer Vehicle Maps Service layer.
+     */
+    VmsClientStats(int uid, VmsLayer layer) {
+        this.mUid = uid;
+
+        this.mLayerType = layer.getType();
+        this.mLayerChannel = layer.getSubtype();
+        this.mLayerVersion = layer.getVersion();
+    }
+
+    /**
+     * Copy constructor for entries exported from {@link VmsClientLog} to {@link CarStatsService}.
+     */
+    VmsClientStats(VmsClientStats other) {
+        synchronized (other.mLock) {
+            this.mUid = other.mUid;
+
+            this.mLayerType = other.mLayerType;
+            this.mLayerChannel = other.mLayerChannel;
+            this.mLayerVersion = other.mLayerVersion;
+
+            this.mTxBytes = other.mTxBytes;
+            this.mTxPackets = other.mTxPackets;
+            this.mRxBytes = other.mRxBytes;
+            this.mRxPackets = other.mRxPackets;
+            this.mDroppedBytes = other.mDroppedBytes;
+            this.mDroppedPackets = other.mDroppedPackets;
+        }
+    }
+
+    /**
+     * Records that a packet was sent by a publisher client.
+     *
+     * @param size Size of packet.
+     */
+    void packetSent(long size) {
+        synchronized (mLock) {
+            mTxBytes += size;
+            mTxPackets++;
+        }
+    }
+
+    /**
+     * Records that a packet was successfully received by a subscriber client.
+     *
+     * @param size Size of packet.
+     */
+    void packetReceived(long size) {
+        synchronized (mLock) {
+            mRxBytes += size;
+            mRxPackets++;
+        }
+    }
+
+    /**
+     * Records that a packet was dropped while being delivered to a subscriber client.
+     *
+     * @param size Size of packet.
+     */
+    void packetDropped(long size) {
+        synchronized (mLock) {
+            mDroppedBytes += size;
+            mDroppedPackets++;
+        }
+    }
+
+    int getUid() {
+        return mUid;
+    }
+
+    int getLayerType() {
+        return mLayerType;
+    }
+
+    int getLayerChannel() {
+        return mLayerChannel;
+    }
+
+    int getLayerVersion() {
+        return mLayerVersion;
+    }
+
+    long getTxBytes() {
+        synchronized (mLock) {
+            return mTxBytes;
+        }
+    }
+
+    long getTxPackets() {
+        synchronized (mLock) {
+            return mTxPackets;
+        }
+    }
+
+    long getRxBytes() {
+        synchronized (mLock) {
+            return mRxBytes;
+        }
+    }
+
+    long getRxPackets() {
+        synchronized (mLock) {
+            return mRxPackets;
+        }
+    }
+
+    long getDroppedBytes() {
+        synchronized (mLock) {
+            return mDroppedBytes;
+        }
+    }
+
+    long getDroppedPackets() {
+        synchronized (mLock) {
+            return mDroppedPackets;
+        }
+    }
+}
+
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 04e218f..5b03820 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -30,6 +30,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -40,6 +41,9 @@
 import com.android.car.R;
 import com.android.car.VmsPublisherService;
 import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
+import com.android.car.stats.VmsClientLog.ConnectionState;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -49,7 +53,6 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.IntSupplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -68,11 +71,12 @@
 
     private final Context mContext;
     private final PackageManager mPackageManager;
-    private final Handler mHandler;
     private final UserManager mUserManager;
     private final CarUserService mUserService;
-    private final int mMillisBeforeRebind;
+    private final CarStatsService mStatsService;
+    private final Handler mHandler;
     private final IntSupplier mGetCallingUid;
+    private final int mMillisBeforeRebind;
 
     private final Object mLock = new Object();
 
@@ -96,9 +100,6 @@
     @GuardedBy("mLock")
     private final Map<IBinder, SubscriberConnection> mSubscribers = new HashMap<>();
 
-    @GuardedBy("mRebindCounts")
-    private final Map<String, AtomicLong> mRebindCounts = new ArrayMap<>();
-
     @VisibleForTesting
     final Runnable mSystemUserUnlockedListener = () -> {
         synchronized (mLock) {
@@ -111,7 +112,10 @@
     final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
         @Override
         public void onUserLockChanged(int userId, boolean unlocked) {
-            //Do nothing.
+            if (unlocked) {
+                if (DBG) Log.d(TAG, "onUserLockChanged: " + userId);
+                switchUser();
+            }
         }
 
         @Override
@@ -146,30 +150,34 @@
      * Constructor for client manager.
      *
      * @param context           Context to use for registering receivers and binding services.
-     * @param brokerService     Service managing the VMS publisher/subscriber state.
+     * @param statsService      Service for logging client metrics.
      * @param userService       User service for registering system unlock listener.
+     * @param brokerService     Service managing the VMS publisher/subscriber state.
      * @param halService        Service providing the HAL client interface
      */
-    public VmsClientManager(Context context, VmsBrokerService brokerService,
-            CarUserService userService, VmsHalService halService) {
-        this(context, brokerService, userService, halService,
+    public VmsClientManager(Context context, CarStatsService statsService,
+            CarUserService userService, VmsBrokerService brokerService,
+            VmsHalService halService) {
+        this(context, statsService, userService, brokerService, halService,
                 new Handler(Looper.getMainLooper()), Binder::getCallingUid);
     }
 
     @VisibleForTesting
-    VmsClientManager(Context context, VmsBrokerService brokerService,
-            CarUserService userService, VmsHalService halService, Handler handler,
-            IntSupplier getCallingUid) {
+    VmsClientManager(Context context, CarStatsService statsService,
+            CarUserService userService, VmsBrokerService brokerService,
+            VmsHalService halService, Handler handler, IntSupplier getCallingUid) {
         mContext = context;
         mPackageManager = context.getPackageManager();
-        mHandler = handler;
-        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mStatsService = statsService;
         mUserService = userService;
         mCurrentUser = ActivityManager.getCurrentUser();
         mBrokerService = brokerService;
-        mMillisBeforeRebind = mContext.getResources().getInteger(
-                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+        mHandler = handler;
         mGetCallingUid = getCallingUid;
+        mMillisBeforeRebind = context.getResources().getInteger(
+                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+
         halService.setClientManager(this);
     }
 
@@ -186,7 +194,6 @@
 
     @Override
     public void init() {
-        new Exception("initiaVmsManager").printStackTrace();
         mUserService.runOnUser0Unlock(mSystemUserUnlockedListener);
         mUserService.addUserCallback(mUserCallback);
     }
@@ -206,11 +213,6 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        dumpMetrics(writer);
-    }
-
-    @Override
-    public void dumpMetrics(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
         synchronized (mLock) {
             writer.println("mCurrentUser:" + mCurrentUser);
@@ -226,12 +228,6 @@
                 writer.printf("\t%s\n", subscriber);
             }
         }
-        synchronized (mRebindCounts) {
-            writer.println("mRebindCounts:");
-            for (Map.Entry<String, AtomicLong> entry : mRebindCounts.entrySet()) {
-                writer.printf("\t%s: %s\n", entry.getKey(), entry.getValue());
-            }
-        }
     }
 
 
@@ -242,7 +238,8 @@
      */
     public void addSubscriber(IVmsSubscriberClient subscriberClient) {
         if (subscriberClient == null) {
-            Log.e(TAG, "Trying to add a null subscriber: " + getCallingPackage());
+            Log.e(TAG, "Trying to add a null subscriber: "
+                    + getCallingPackage(mGetCallingUid.getAsInt()));
             throw new IllegalArgumentException("subscriber cannot be null.");
         }
 
@@ -253,13 +250,14 @@
                 return;
             }
 
-            int subscriberUserId = UserHandle.getUserId(mGetCallingUid.getAsInt());
+            int callingUid = mGetCallingUid.getAsInt();
+            int subscriberUserId = UserHandle.getUserId(callingUid);
             if (subscriberUserId != mCurrentUser && subscriberUserId != UserHandle.USER_SYSTEM) {
                 throw new SecurityException("Caller must be foreground user or system");
             }
 
             SubscriberConnection subscriber = new SubscriberConnection(
-                    subscriberClient, getCallingPackage(), subscriberUserId);
+                    subscriberClient, callingUid, getCallingPackage(callingUid), subscriberUserId);
             if (DBG) Log.d(TAG, "Registering subscriber: " + subscriber);
             try {
                 subscriberBinder.linkToDeath(subscriber, 0);
@@ -296,6 +294,16 @@
     }
 
     /**
+     * Gets the application UID associated with a subscriber client.
+     */
+    public int getSubscriberUid(IVmsSubscriberClient subscriberClient) {
+        synchronized (mLock) {
+            SubscriberConnection subscriber = mSubscribers.get(subscriberClient.asBinder());
+            return subscriber != null ? subscriber.mUid : Process.INVALID_UID;
+        }
+    }
+
+    /**
      * Gets the package name for a given subscriber client.
      */
     public String getPackageName(IVmsSubscriberClient subscriberClient) {
@@ -307,9 +315,6 @@
 
     /**
      * Registers the HAL client connections.
-     *
-     * @param publisherClient
-     * @param subscriberClient
      */
     public void onHalConnected(IVmsPublisherClient publisherClient,
             IVmsSubscriberClient subscriberClient) {
@@ -317,9 +322,11 @@
             mHalClient = publisherClient;
             mPublisherService.onClientConnected(HAL_CLIENT_NAME, mHalClient);
             mSubscribers.put(subscriberClient.asBinder(),
-                    new SubscriberConnection(subscriberClient, HAL_CLIENT_NAME,
+                    new SubscriberConnection(subscriberClient, Process.myUid(), HAL_CLIENT_NAME,
                             UserHandle.USER_SYSTEM));
         }
+        mStatsService.getVmsClientLog(Process.myUid())
+                .logConnectionState(ConnectionState.CONNECTED);
     }
 
     /**
@@ -329,14 +336,13 @@
         synchronized (mLock) {
             if (mHalClient != null) {
                 mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
+                mStatsService.getVmsClientLog(Process.myUid())
+                        .logConnectionState(ConnectionState.DISCONNECTED);
             }
             mHalClient = null;
             terminate(mSubscribers.values().stream()
                     .filter(subscriber -> HAL_CLIENT_NAME.equals(subscriber.mPackageName)));
         }
-        synchronized (mRebindCounts) {
-            mRebindCounts.computeIfAbsent(HAL_CLIENT_NAME, k -> new AtomicLong()).incrementAndGet();
-        }
     }
 
     private void dumpConnections(PrintWriter writer,
@@ -404,13 +410,17 @@
             return;
         }
 
+        VmsClientLog statsLog = mStatsService.getVmsClientLog(
+                UserHandle.getUid(userHandle.getIdentifier(), serviceInfo.applicationInfo.uid));
+
         if (!Car.PERMISSION_BIND_VMS_CLIENT.equals(serviceInfo.permission)) {
             Log.e(TAG, "Client service: " + clientName
                     + " does not require " + Car.PERMISSION_BIND_VMS_CLIENT + " permission");
+            statsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
             return;
         }
 
-        PublisherConnection connection = new PublisherConnection(name, userHandle);
+        PublisherConnection connection = new PublisherConnection(name, userHandle, statsLog);
         if (connection.bind()) {
             Log.i(TAG, "Client bound: " + connection);
             connectionMap.put(clientName, connection);
@@ -428,15 +438,17 @@
         private final ComponentName mName;
         private final UserHandle mUser;
         private final String mFullName;
+        private final VmsClientLog mStatsLog;
         private boolean mIsBound = false;
         private boolean mIsTerminated = false;
         private boolean mRebindScheduled = false;
         private IVmsPublisherClient mClientService;
 
-        PublisherConnection(ComponentName name, UserHandle user) {
+        PublisherConnection(ComponentName name, UserHandle user, VmsClientLog statsLog) {
             mName = name;
             mUser = user;
             mFullName = mName.flattenToString() + " U=" + mUser.getIdentifier();
+            mStatsLog = statsLog;
         }
 
         synchronized boolean bind() {
@@ -446,6 +458,7 @@
             if (mIsTerminated) {
                 return false;
             }
+            mStatsLog.logConnectionState(ConnectionState.CONNECTING);
 
             if (DBG) Log.d(TAG, "binding: " + mFullName);
             Intent intent = new Intent();
@@ -457,6 +470,10 @@
                 Log.e(TAG, "While binding " + mFullName, e);
             }
 
+            if (!mIsBound) {
+                mStatsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
+            }
+
             return mIsBound;
         }
 
@@ -500,23 +517,20 @@
             // If the client is not currently bound, unbind() will have no effect.
             unbind();
             bind();
-            synchronized (mRebindCounts) {
-                mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
-                        .incrementAndGet();
-            }
         }
 
         synchronized void terminate() {
             if (DBG) Log.d(TAG, "terminating: " + mFullName);
             mIsTerminated = true;
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.TERMINATED);
             unbind();
         }
 
-        synchronized void notifyOnDisconnect() {
+        synchronized void notifyOnDisconnect(int connectionState) {
             if (mClientService != null) {
                 mPublisherService.onClientDisconnected(mFullName);
                 mClientService = null;
+                mStatsLog.logConnectionState(connectionState);
             }
         }
 
@@ -525,19 +539,20 @@
             if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
             mClientService = IVmsPublisherClient.Stub.asInterface(service);
             mPublisherService.onClientConnected(mFullName, mClientService);
+            mStatsLog.logConnectionState(ConnectionState.CONNECTED);
         }
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
             if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.DISCONNECTED);
             scheduleRebind();
         }
 
         @Override
         public void onBindingDied(ComponentName name) {
             if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
-            notifyOnDisconnect();
+            notifyOnDisconnect(ConnectionState.DISCONNECTED);
             scheduleRebind();
         }
 
@@ -553,8 +568,8 @@
     }
 
     // If we're in a binder call, returns back the package name of the caller of the binder call.
-    private String getCallingPackage() {
-        String packageName = mPackageManager.getNameForUid(mGetCallingUid.getAsInt());
+    private String getCallingPackage(int uid) {
+        String packageName = mPackageManager.getNameForUid(uid);
         if (packageName == null) {
             return UNKNOWN_PACKAGE;
         } else {
@@ -564,12 +579,14 @@
 
     private class SubscriberConnection implements IBinder.DeathRecipient {
         private final IVmsSubscriberClient mClient;
+        private final int mUid;
         private final String mPackageName;
         private final int mUserId;
 
-        SubscriberConnection(IVmsSubscriberClient subscriberClient, String packageName,
+        SubscriberConnection(IVmsSubscriberClient subscriberClient, int uid, String packageName,
                 int userId) {
             mClient = subscriberClient;
+            mUid = uid;
             mPackageName = packageName;
             mUserId = userId;
         }
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/PrivateVolumeSettings.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/PrivateVolumeSettings.java
index 3cd45c5..31d13c3 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/PrivateVolumeSettings.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/PrivateVolumeSettings.java
@@ -762,7 +762,7 @@
         public Dialog onCreateDialog(Bundle savedInstanceState) {
             return new AlertDialog.Builder(getActivity())
                     .setMessage(getContext().getString(R.string.storage_detail_dialog_system,
-                            Build.VERSION.RELEASE_OR_CODENAME))
+                            Build.VERSION.RELEASE))
                     .setPositiveButton(android.R.string.ok, null)
                     .create();
         }
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
index 4ee76b8..24b1e29 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
@@ -70,7 +70,7 @@
 
     @Override
     public CharSequence getSummary() {
-        return Build.VERSION.RELEASE_OR_CODENAME;
+        return Build.VERSION.RELEASE;
     }
 
     @Override
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java
index 789d047..e9f70e6 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java
@@ -34,6 +34,6 @@
 
     @Override
     public CharSequence getSummary() {
-        return Build.VERSION.RELEASE_OR_CODENAME;
+        return Build.VERSION.RELEASE;
     }
 }
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/system/SystemUpdatePreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/system/SystemUpdatePreferenceController.java
index efa06ea..9209d3f 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/system/SystemUpdatePreferenceController.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/system/SystemUpdatePreferenceController.java
@@ -89,7 +89,7 @@
     @Override
     public CharSequence getSummary() {
         CharSequence summary = mContext.getString(R.string.android_version_summary,
-                Build.VERSION.RELEASE_OR_CODENAME);
+                Build.VERSION.RELEASE);
         final FutureTask<Bundle> bundleFutureTask = new FutureTask<>(
                 // Put the API call in a future to avoid StrictMode violation.
                 () -> mUpdateManager.retrieveSystemUpdateInfo());
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 2fbeef9..dcfffd1 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -158,6 +158,27 @@
                   android:grantUriPermissions="true"
                   android:exported="true"/>
 
+        <activity android:name=".AlwaysCrashingActivity"
+                  android:label="@string/always_crashing_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".NoCrashActivity"
+                  android:label="@string/no_crash_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".EmptyActivity"
+                  android:label="@string/empty_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
 
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
new file mode 100644
index 0000000..5312fee
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/empty_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/empty_activity"
+        android:layout_weight="1" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 6575ce1..0d56369 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -332,4 +332,9 @@
     <string name="usernotice" translatable="false">This screen is for showing initial user notice and is not for product. Plz change config_userNoticeUiService in CarService before shipping.</string>
     <string name="dismiss_now" translatable="false">Dismiss for now</string>
     <string name="dismiss_forever" translatable="false">Do not show again</string>
+
+    <!-- [AlwaysCrashing|NoCrash|Empty]Activity -->
+    <string name="always_crashing_activity" translatable="false">Always Crash Activity</string>
+    <string name="no_crash_activity" translatable="false">No Crash Activity</string>
+    <string name="empty_activity" translatable="false">Empty Activity</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
new file mode 100644
index 0000000..05529b2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Activity for testing purpose. This one always crashes inside onCreate.
+ */
+public class AlwaysCrashingActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        throw new RuntimeException("Intended crash for testing");
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
new file mode 100644
index 0000000..aad25cb
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class EmptyActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.empty_activity);
+    }
+
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
new file mode 100644
index 0000000..f10e1db
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class NoCrashActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.empty_activity);
+        TextView text = findViewById(R.id.empty_text);
+        text.setText(R.string.no_crash_activity);
+    }
+}
diff --git a/tests/android_car_api_test/Android.mk b/tests/android_car_api_test/Android.mk
index c292542..6eddb90 100644
--- a/tests/android_car_api_test/Android.mk
+++ b/tests/android_car_api_test/Android.mk
@@ -39,6 +39,9 @@
         android.hidl.base-V1.0-java \
         android.hardware.automotive.vehicle-V2.0-java \
         android.car.cluster.navigation \
+        compatibility-device-util-axt \
+        testng \
+        truth-prebuilt \
 
 LOCAL_JAVA_LIBRARIES := android.car android.test.runner android.test.base
 
diff --git a/tests/android_car_api_test/AndroidManifest.xml b/tests/android_car_api_test/AndroidManifest.xml
index 3339d28..301ec86 100644
--- a/tests/android_car_api_test/AndroidManifest.xml
+++ b/tests/android_car_api_test/AndroidManifest.xml
@@ -20,7 +20,9 @@
         android:sharedUserId="com.google.android.car.uid.kitchensink"
         android:debuggable="true" >
 
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="android.car.apitest"
             android:label="Tests for Car APIs"
             android:debuggable="true" />
@@ -35,5 +37,7 @@
         </activity>
         <service android:name=".CarProjectionManagerTest$TestService"
                  android:exported="true" />
+        <activity android:name=".CarActivityViewDisplayIdTest$ActivityInActivityView"/>
+        <activity android:name=".CarActivityViewDisplayIdTest$ActivityViewTestActivity"/>
     </application>
 </manifest>
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java b/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java
new file mode 100644
index 0000000..e82fa65
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.apitest;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityView;
+import android.app.Instrumentation;
+import android.car.Car;
+import android.car.app.CarActivityView;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.Display;
+import android.view.ViewGroup;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.MethodSorters;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Build/Install/Run:
+ *  atest AndroidCarApiTest:CarActivityViewDisplayIdTest
+ */
+@RunWith(JUnit4.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@MediumTest
+public class CarActivityViewDisplayIdTest extends CarApiTestBase {
+    private static final String CAR_LAUNCHER_PKG_NAME = "com.android.car.carlauncher";
+    private static final String ACTIVITY_VIEW_DISPLAY_NAME = "TaskVirtualDisplay";
+    private static final String AM_START_HOME_ACTIVITY_COMMAND =
+            "am start -a android.intent.action.MAIN -c android.intent.category.HOME";
+    private static final int NONEXISTENT_DISPLAY_ID = Integer.MAX_VALUE;
+    private static final int TEST_TIMEOUT_SEC = 5;
+    private static final int TEST_TIMEOUT_MS = TEST_TIMEOUT_SEC * 1000;
+
+    private DisplayManager mDisplayManager;
+    private CarUxRestrictionsManager mCarUxRestrictionsManager;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mDisplayManager = getContext().getSystemService(DisplayManager.class);
+        mCarUxRestrictionsManager = (CarUxRestrictionsManager)
+                getCar().getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
+    }
+
+    private int getMappedPhysicalDisplayOfVirtualDisplay(int displayId) {
+        return mCarUxRestrictionsManager.getMappedPhysicalDisplayOfVirtualDisplay(displayId);
+    }
+
+    @Test
+    public void testSingleActivityView() throws Exception {
+        ActivityViewTestActivity activity = startActivityViewTestActivity(DEFAULT_DISPLAY);
+        activity.waitForActivityViewReady();
+        int virtualDisplayId = activity.getActivityView().getVirtualDisplayId();
+
+        startTestActivity(ActivityInActivityView.class, virtualDisplayId);
+
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId))
+                .isEqualTo(DEFAULT_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(DEFAULT_DISPLAY))
+                .isEqualTo(INVALID_DISPLAY);
+
+        activity.finish();
+        activity.waitForActivityViewDestroyed();
+
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId))
+                .isEqualTo(INVALID_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(DEFAULT_DISPLAY))
+                .isEqualTo(INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testDoubleActivityView() throws Exception {
+        ActivityViewTestActivity activity1 = startActivityViewTestActivity(DEFAULT_DISPLAY);
+        activity1.waitForActivityViewReady();
+        int virtualDisplayId1 = activity1.getActivityView().getVirtualDisplayId();
+
+        ActivityViewTestActivity activity2 = startActivityViewTestActivity(virtualDisplayId1);
+        activity2.waitForActivityViewReady();
+        int virtualDisplayId2 = activity2.getActivityView().getVirtualDisplayId();
+
+        startTestActivity(ActivityInActivityView.class, virtualDisplayId2);
+
+        assertThat(virtualDisplayId1).isNotEqualTo(virtualDisplayId2);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId1))
+                .isEqualTo(DEFAULT_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId2))
+                .isEqualTo(DEFAULT_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(DEFAULT_DISPLAY))
+                .isEqualTo(INVALID_DISPLAY);
+
+        activity2.finish();
+        activity1.finish();
+
+        activity2.waitForActivityViewDestroyed();
+        activity1.waitForActivityViewDestroyed();
+
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId1))
+                .isEqualTo(INVALID_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId2))
+                .isEqualTo(INVALID_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(DEFAULT_DISPLAY))
+                .isEqualTo(INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testThrowsExceptionOnReportingNonExistingDisplay() throws Exception {
+        ActivityViewTestActivity activity = startActivityViewTestActivity(DEFAULT_DISPLAY);
+        activity.waitForActivityViewReady();
+        int virtualDisplayId = activity.getActivityView().getVirtualDisplayId();
+
+        // This will pass since the test owns the display.
+        mCarUxRestrictionsManager.reportVirtualDisplayToPhysicalDisplay(virtualDisplayId,
+                NONEXISTENT_DISPLAY_ID);
+
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(virtualDisplayId))
+                .isEqualTo(NONEXISTENT_DISPLAY_ID);
+
+        activity.finish();
+        activity.waitForActivityViewDestroyed();
+
+        // Now the display was released, so expect to throw an Exception.
+        assertThrows(
+                java.lang.IllegalArgumentException.class,
+                () -> mCarUxRestrictionsManager.reportVirtualDisplayToPhysicalDisplay(
+                        virtualDisplayId, NONEXISTENT_DISPLAY_ID));
+    }
+
+    // TODO(b/143353546): Make the following tests not to rely on CarLauncher.
+    @Test
+    public void testThrowsExceptionOnReportingNonOwningDisplay() throws Exception {
+        int displayIdOfCarLauncher = waitForCarLauncherDisplayReady(INVALID_DISPLAY);
+        assumeTrue(INVALID_DISPLAY != displayIdOfCarLauncher);
+
+        // CarLauncher owns the display, so expect to throw an Exception.
+        assertThrows(
+                java.lang.SecurityException.class,
+                () -> mCarUxRestrictionsManager.reportVirtualDisplayToPhysicalDisplay(
+                        displayIdOfCarLauncher, DEFAULT_DISPLAY + 1));
+    }
+
+    // The test name starts with 'testz' to run it at the last among the tests, since killing
+    // CarLauncher causes the system unstable for a while.
+    @Test
+    public void testzCleanUpAfterClientIsCrashed() throws Exception {
+        int displayIdOfCarLauncher = waitForCarLauncherDisplayReady(INVALID_DISPLAY);
+        assumeTrue(INVALID_DISPLAY != displayIdOfCarLauncher);
+
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(displayIdOfCarLauncher))
+                .isEqualTo(DEFAULT_DISPLAY);
+
+        ActivityManager am = getContext().getSystemService(ActivityManager.class);
+        am.forceStopPackage(CAR_LAUNCHER_PKG_NAME);
+        launchHomeActivity();
+        int displayIdOfNewCarLauncher = waitForCarLauncherDisplayReady(displayIdOfCarLauncher);
+
+        assertThat(displayIdOfNewCarLauncher).isNotEqualTo(INVALID_DISPLAY);
+        assertThat(displayIdOfNewCarLauncher).isNotEqualTo(displayIdOfCarLauncher);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(displayIdOfCarLauncher))
+                .isEqualTo(INVALID_DISPLAY);
+        assertThat(getMappedPhysicalDisplayOfVirtualDisplay(displayIdOfNewCarLauncher))
+                .isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    private int findDisplayIdOfCarLauncher() {
+        for (Display display: mDisplayManager.getDisplays()) {
+            String displayName = display.getName();
+            if (display.getName().contains(ACTIVITY_VIEW_DISPLAY_NAME)
+                    && display.getOwnerPackageName().equals(CAR_LAUNCHER_PKG_NAME)) {
+                return display.getDisplayId();
+            }
+        }
+        return INVALID_DISPLAY;
+    }
+
+    /**
+     * Waits until CarLauncher is ready and returns the display id of its ActivityView.
+     * When killing a CarLauncher, findDisplayIdOfCarLauncher() can return the old one for
+     * a while until DisplayManagerService gets the update.
+     * To differentiate it, the method accepts the old display id and ignores it.
+     */
+    private int waitForCarLauncherDisplayReady(int oldDisplayId) {
+        for (int i = 0; i < TEST_TIMEOUT_SEC; ++i) {
+            int displayId = findDisplayIdOfCarLauncher();
+            if (displayId != INVALID_DISPLAY && displayId != oldDisplayId) {
+                return displayId;
+            }
+            SystemClock.sleep(/*ms=*/ 1000);
+        }
+        return findDisplayIdOfCarLauncher();
+    }
+
+    private static class TestActivity extends Activity {
+        private final CountDownLatch mResumed = new CountDownLatch(1);
+
+        @Override
+        protected void onPostResume() {
+            super.onPostResume();
+            mResumed.countDown();
+        }
+
+        void waitForResumeStateChange() throws Exception {
+            waitForLatch(mResumed);
+        }
+    }
+
+    private static void waitForLatch(CountDownLatch latch) throws Exception {
+        boolean result = latch.await(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
+        if (!result) {
+            throw new TimeoutException("Timed out waiting for task stack change notification");
+        }
+    }
+
+    /**
+     * Starts the provided activity and returns the started instance.
+     */
+    private TestActivity startTestActivity(Class<?> activityClass, int displayId) throws Exception {
+        Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+                activityClass.getName(), null, false);
+        getInstrumentation().addMonitor(monitor);
+
+        Context context = getContext();
+        Intent intent = new Intent(context, activityClass).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ActivityOptions options = ActivityOptions.makeBasic();
+        if (displayId != DEFAULT_DISPLAY) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+            options.setLaunchDisplayId(displayId);
+        }
+        context.startActivity(intent, options.toBundle());
+
+        TestActivity activity = (TestActivity) monitor.waitForActivityWithTimeout(TEST_TIMEOUT_MS);
+        if (activity == null) {
+            throw new TimeoutException("Timed out waiting for Activity");
+        }
+        activity.waitForResumeStateChange();
+        return activity;
+    }
+
+    public static final class ActivityViewTestActivity extends TestActivity {
+        private static final class ActivityViewStateCallback extends ActivityView.StateCallback {
+            private final CountDownLatch mActivityViewReadyLatch = new CountDownLatch(1);
+            private final CountDownLatch mActivityViewDestroyedLatch = new CountDownLatch(1);
+
+            @Override
+            public void onActivityViewReady(ActivityView view) {
+                mActivityViewReadyLatch.countDown();
+            }
+
+            @Override
+            public void onActivityViewDestroyed(ActivityView view) {
+                mActivityViewDestroyedLatch.countDown();
+            }
+        }
+
+        private CarActivityView mActivityView;
+        private final ActivityViewStateCallback mCallback = new ActivityViewStateCallback();
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+
+            mActivityView = new CarActivityView(this, /*attrs=*/null , /*defStyle=*/0 ,
+                    /*singleTaskInstance=*/true);
+            mActivityView.setCallback(mCallback);
+            setContentView(mActivityView);
+
+            ViewGroup.LayoutParams layoutParams = mActivityView.getLayoutParams();
+            layoutParams.width = MATCH_PARENT;
+            layoutParams.height = MATCH_PARENT;
+            mActivityView.requestLayout();
+        }
+
+        @Override
+        protected void onDestroy() {
+            mActivityView.release();
+            super.onDestroy();
+        }
+
+        ActivityView getActivityView() {
+            return mActivityView;
+        }
+
+        void waitForActivityViewReady() throws Exception {
+            waitForLatch(mCallback.mActivityViewReadyLatch);
+        }
+
+        void waitForActivityViewDestroyed() throws Exception {
+            waitForLatch(mCallback.mActivityViewDestroyedLatch);
+        }
+    }
+
+    private ActivityViewTestActivity startActivityViewTestActivity(int displayId) throws Exception {
+        return (ActivityViewTestActivity) startTestActivity(ActivityViewTestActivity.class,
+                displayId);
+    }
+
+    // Activity that has {@link android.R.attr#resizeableActivity} attribute set to {@code true}
+    public static class ActivityInActivityView extends TestActivity {}
+
+    private ActivityInActivityView startActivityInActivityView(int displayId) throws Exception {
+        return (ActivityInActivityView) startTestActivity(ActivityInActivityView.class, displayId);
+    }
+
+    private void launchHomeActivity() {
+        try {
+            SystemUtil.runShellCommand(getInstrumentation(), AM_START_HOME_ACTIVITY_COMMAND);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarApiTestBase.java b/tests/android_car_api_test/src/android/car/apitest/CarApiTestBase.java
index dc167b1..d78d8d6 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarApiTestBase.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarApiTestBase.java
@@ -16,13 +16,15 @@
 
 package android.car.apitest;
 
+import android.car.Car;
 import android.content.ComponentName;
 import android.content.ServiceConnection;
 import android.os.IBinder;
 import android.os.Looper;
-import android.car.Car;
 import android.test.AndroidTestCase;
 
+import androidx.test.InstrumentationRegistry;
+
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
@@ -41,6 +43,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        setContext(InstrumentationRegistry.getContext());
         mCar = Car.createCar(getContext(), mConnectionListener);
         mCar.connect();
         mConnectionListener.waitForConnection(DEFAULT_WAIT_TIMEOUT_MS);
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 15adadf..00f1243 100644
--- a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
@@ -16,8 +16,6 @@
 
 package com.android.car;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
@@ -45,24 +43,23 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
 import com.android.car.vms.VmsBrokerService;
 import com.android.car.vms.VmsClientManager;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.List;
 
 @RunWith(MockitoJUnitRunner.class)
 @SmallTest
@@ -72,18 +69,20 @@
     private static final VmsLayersOffering OFFERING = new VmsLayersOffering(Collections.emptySet(),
             54321);
     private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
-    private static final VmsLayer LAYER2 = new VmsLayer(2, 2, 8);
-    private static final VmsLayer LAYER3 = new VmsLayer(3, 2, 8);
-    private static final VmsLayer LAYER4 = new VmsLayer(4, 2, 8);
 
     private static final int PUBLISHER_ID = 54321;
     private static final byte[] PAYLOAD = new byte[]{1, 2, 3, 4};
-    private static final byte[] PAYLOAD2 = new byte[]{1, 2, 3, 4, 5, 6};
-    private static final byte[] PAYLOAD3 = new byte[]{10, 12, 93, 4, 5, 6, 1, 1, 1};
+
+    private static final int PUBLISHER_UID = 10100;
+    private static final int SUBSCRIBER_UID = 10101;
+    private static final int SUBSCRIBER_UID2 = 10102;
+    private static final int NO_SUBSCRIBERS_UID = -1;
 
     @Mock
     private Context mContext;
     @Mock
+    private CarStatsService mStatsService;
+    @Mock
     private VmsBrokerService mBrokerService;
     @Captor
     private ArgumentCaptor<VmsBrokerService.PublisherListener> mProxyCaptor;
@@ -91,13 +90,18 @@
     private VmsClientManager mClientManager;
 
     @Mock
+    private VmsClientLog mPublisherLog;
+    @Mock
+    private VmsClientLog mSubscriberLog;
+    @Mock
+    private VmsClientLog mSubscriberLog2;
+    @Mock
+    private VmsClientLog mNoSubscribersLog;
+
+    @Mock
     private IVmsSubscriberClient mSubscriberClient;
     @Mock
     private IVmsSubscriberClient mSubscriberClient2;
-    @Mock
-    private IVmsSubscriberClient mThrowingSubscriberClient;
-    @Mock
-    private IVmsSubscriberClient mThrowingSubscriberClient2;
 
     private VmsPublisherService mPublisherService;
     private MockPublisherClient mPublisherClient;
@@ -105,14 +109,27 @@
 
     @Before
     public void setUp() {
-        mPublisherService = new VmsPublisherService(mContext, mBrokerService, mClientManager);
+        mPublisherService = new VmsPublisherService(mContext, mStatsService, mBrokerService,
+                mClientManager, () -> PUBLISHER_UID);
         verify(mClientManager).setPublisherService(mPublisherService);
 
+        when(mClientManager.getSubscriberUid(mSubscriberClient)).thenReturn(SUBSCRIBER_UID);
+        when(mClientManager.getSubscriberUid(mSubscriberClient2)).thenReturn(SUBSCRIBER_UID2);
+
+        when(mStatsService.getVmsClientLog(PUBLISHER_UID)).thenReturn(mPublisherLog);
+        when(mStatsService.getVmsClientLog(SUBSCRIBER_UID)).thenReturn(mSubscriberLog);
+        when(mStatsService.getVmsClientLog(SUBSCRIBER_UID2)).thenReturn(mSubscriberLog2);
+        when(mStatsService.getVmsClientLog(NO_SUBSCRIBERS_UID)).thenReturn(mNoSubscribersLog);
+
         mPublisherClient = new MockPublisherClient();
         mPublisherClient2 = new MockPublisherClient();
         when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
                 .thenReturn(new HashSet<>(Arrays.asList(mSubscriberClient, mSubscriberClient2)));
+    }
 
+    @After
+    public void tearDown() {
+        verifyNoMoreInteractions(mPublisherLog, mSubscriberLog, mSubscriberLog2, mNoSubscribersLog);
     }
 
     @Test
@@ -188,6 +205,10 @@
                 PAYLOAD);
         verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
         verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog).logPacketReceived(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
     }
 
     @Test
@@ -200,6 +221,19 @@
     }
 
     @Test
+    public void testPublish_NoSubscribers() throws Exception {
+        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
+        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
+                .thenReturn(Collections.emptySet());
+
+        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
+                PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mNoSubscribersLog).logPacketDropped(LAYER, PAYLOAD.length);
+    }
+
+    @Test
     public void testPublish_ClientError() throws Exception {
         mPublisherService.onClientConnected("SomeClient", mPublisherClient);
         doThrow(new RemoteException()).when(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
@@ -208,6 +242,10 @@
                 PAYLOAD);
         verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
         verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+        verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog).logPacketDropped(LAYER, PAYLOAD.length);
+        verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
     }
 
     @Test(expected = SecurityException.class)
@@ -299,341 +337,6 @@
     }
 
     @Test
-    public void testDump_getPacketCount() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-        String expectedPacketCountString = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER, 1L);
-        String expectedPacketSizeString = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER, PAYLOAD.length);
-        assertThat(dumpString.contains(expectedPacketCountString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDump_getPacketCounts() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD2);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        // LAYER called 6 times with PAYLOAD 2 times, PAYLOAD2 1 time, PAYLOAD3 3 times
-        String expectedPacketCountString1 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER, 6L);
-        String expectedPacketSizeString1 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER, 2 * PAYLOAD.length + PAYLOAD2.length + 3 * PAYLOAD3.length);
-
-        // LAYER2 called 2 times with PAYLOAD 1 time, PAYLOAD2 0 time, PAYLOAD3 1 times
-        String expectedPacketCountString2 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER2, 2L);
-        String expectedPacketSizeString2 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER2, PAYLOAD.length + PAYLOAD3.length);
-
-        // LAYER3 called 2 times with PAYLOAD 2 times, PAYLOAD2 0 time, PAYLOAD3 0 times
-        String expectedPacketCountString3 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
-                LAYER3, 2L);
-        String expectedPacketSizeString3 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
-                LAYER3, 2 * PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketCountString1)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString1)).isTrue();
-        assertThat(dumpString.contains(expectedPacketCountString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketCountString3)).isTrue();
-        assertThat(dumpString.contains(expectedPacketSizeString3)).isTrue();
-    }
-
-    @Test
-    public void testDumpNoListeners_getPacketFailureCount() throws Exception {
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER2, "SomeClient", "", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER2, "SomeClient", "", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDumpNoListeners_getPacketFailureCounts() throws Exception {
-        // LAYER2 and LAYER3 both have no listeners
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER2, PUBLISHER_ID))
-                .thenReturn(new HashSet<>());
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>());
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER2, "SomeClient", "", 1L);
-        String expectedPacketFailureString2 = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER3, "SomeClient2", "", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER2, "SomeClient", "", PAYLOAD.length);
-        String expectedPacketFailureSizeString2 = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER3, "SomeClient2", "", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureString2)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString2)).isTrue();
-    }
-
-    @Test
-    public void testDumpRemoteException_getPacketFailureCount() throws Exception {
-        // The listener on LAYER3 will throw on LAYER3 and PAYLOAD
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER3, PAYLOAD);
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(Arrays.asList(mThrowingSubscriberClient)));
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        String expectedPacketFailureString = String.format(
-                VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
-                LAYER3, "SomeClient", "Thrower", 1L);
-        String expectedPacketFailureSizeString = String.format(
-                VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
-                LAYER3, "SomeClient", "Thrower", PAYLOAD.length);
-
-        assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
-        assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
-    }
-
-    @Test
-    public void testDumpRemoteException_getPacketFailureCounts() throws Exception {
-        // The listeners will throw on LAYER3 or LAYER4 and PAYLOAD
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER3, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER4, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(
-                mThrowingSubscriberClient2).onVmsMessageReceived(LAYER3, PAYLOAD);
-        Mockito.doThrow(new RemoteException()).when(
-                mThrowingSubscriberClient2).onVmsMessageReceived(LAYER4, PAYLOAD);
-
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
-
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-        when(mClientManager.getPackageName(mThrowingSubscriberClient2)).thenReturn("Thrower2");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
-        // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        List<String> expectedStrings = Arrays.asList(
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
-                        "SomeClient2",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
-                        "SomeClient2",
-                        "Thrower2", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
-                        "SomeClient2",
-                        "Thrower", 2L),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
-                        "SomeClient2",
-                        "Thrower2", 2L),
-
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
-                        "Thrower2", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
-                        "Thrower", 2 * PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
-                        "Thrower2", 2 * PAYLOAD.length));
-
-        for (String expected : expectedStrings) {
-            assertThat(dumpString.contains(expected)).isTrue();
-        }
-    }
-
-    @Test
-    public void testDump_getAllMetrics() throws Exception {
-
-        // LAYER3 has no subscribers
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(Arrays.asList()));
-
-        // LAYER4 has a subscriber that will always throw
-        Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
-                LAYER4, PAYLOAD);
-
-        when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
-                .thenReturn(new HashSet<>(
-                        Arrays.asList(mThrowingSubscriberClient)));
-
-        when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
-        mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
-                PAYLOAD2);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
-                PAYLOAD3);
-        mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
-                PAYLOAD);
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        PrintWriter printWriter = new PrintWriter(outputStream);
-        mPublisherService.dump(printWriter);
-
-        printWriter.flush();
-        String dumpString = outputStream.toString();
-
-        List<String> expectedStrings = Arrays.asList(
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER, 2),
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER3, 1),
-                String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER4, 1),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER,
-                        PAYLOAD.length + PAYLOAD2.length),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER3, PAYLOAD3.length),
-                String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER4, PAYLOAD.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
-                        "",
-                        1),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
-                        "",
-                        PAYLOAD3.length),
-                String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", 1),
-                String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
-                        "Thrower", PAYLOAD.length)
-        );
-
-        for (String expected : expectedStrings) {
-            assertThat(dumpString.contains(expected)).isTrue();
-        }
-    }
-
-
-    @Test
     public void testRelease() {
         mPublisherService.release();
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
new file mode 100644
index 0000000..a18c88f
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.stats;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.car.vms.VmsLayer;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.car.stats.VmsClientLog.ConnectionState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class CarStatsServiceTest {
+    private static final int CLIENT_UID = 10101;
+    private static final int CLIENT_UID2 = 10102;
+    private static final String CLIENT_PACKAGE = "test.package";
+    private static final String CLIENT_PACKAGE2 = "test.package2";
+    private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
+    private static final VmsLayer LAYER2 = new VmsLayer(2, 3, 4);
+    private static final VmsLayer LAYER3 = new VmsLayer(3, 4, 5);
+
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+
+    private CarStatsService mCarStatsService;
+    private StringWriter mDumpsysOutput;
+    private PrintWriter mDumpsysWriter;
+
+    @Before
+    public void setUp() {
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getNameForUid(CLIENT_UID)).thenReturn(CLIENT_PACKAGE);
+        when(mPackageManager.getNameForUid(CLIENT_UID2)).thenReturn(CLIENT_PACKAGE2);
+
+        mCarStatsService = new CarStatsService(mContext);
+        mDumpsysOutput = new StringWriter();
+        mDumpsysWriter = new PrintWriter(mDumpsysOutput);
+    }
+
+    @Test
+    public void testEmptyStats() {
+        mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+        assertEquals(
+                "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+                        + "\nuid,layerType,layerChannel,layerVersion,"
+                        + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n",
+                mDumpsysOutput.toString());
+    }
+
+    @Test
+    public void testLogConnectionState_Connecting() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        validateConnectionStats("10101,test.package,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Connected() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        validateConnectionStats("10101,test.package,0,1,0,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Disconnected() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        validateConnectionStats("10101,test.package,0,0,1,0,0");
+    }
+
+    @Test
+    public void testLogConnectionState_Terminated() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.TERMINATED);
+        validateConnectionStats("10101,test.package,0,0,0,1,0");
+    }
+
+    @Test
+    public void testLogConnectionState_ConnectionError() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTION_ERROR);
+        validateConnectionStats("10101,test.package,0,0,0,0,1");
+    }
+
+    @Test
+    public void testLogConnectionState_UnknownUID() {
+        mCarStatsService.getVmsClientLog(-1)
+                .logConnectionState(ConnectionState.CONNECTING);
+        testEmptyStats();
+    }
+
+    @Test
+    public void testLogConnectionState_MultipleClients_MultipleStates() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.DISCONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logConnectionState(ConnectionState.CONNECTED);
+
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.TERMINATED);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTING);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logConnectionState(ConnectionState.CONNECTION_ERROR);
+        validateConnectionStats(
+                "10101,test.package,1,2,1,0,0\n"
+                + "10102,test.package2,2,1,0,1,1");
+    }
+
+    @Test
+    public void testLogPacketSent() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 5);
+        validateClientStats("10101,1,2,3,5,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,6,3,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,3,1,0,0,0,0\n"
+                        + "10101,2,3,4,2,1,0,0,0,0\n"
+                        + "10101,3,4,5,1,1,0,0,0,0");
+    }
+
+    @Test
+    public void testLogPacketSent_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,3,1,0,0,0,0\n"
+                        + "10102,1,2,3,2,1,0,0,0,0\n"
+                        + "10102,2,3,4,1,1,0,0,0,0\n");
+    }
+
+    @Test
+    public void testLogPacketReceived() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,5,1,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,6,3,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,0,0,3,1,0,0\n"
+                        + "10101,2,3,4,0,0,2,1,0,0\n"
+                        + "10101,3,4,5,0,0,1,1,0,0");
+    }
+
+    @Test
+    public void testLogPacketReceived_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,0,0,3,1,0,0\n"
+                        + "10102,1,2,3,0,0,2,1,0,0\n"
+                        + "10102,2,3,4,0,0,1,1,0,0\n");
+    }
+
+    @Test
+    public void testLogPacketDropped() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 5);
+        validateClientStats("10101,1,2,3,0,0,0,0,5,1");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultiplePackets() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+
+        validateClientStats("10101,1,2,3,0,0,0,0,6,3");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultipleLayers() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER3, 1);
+
+        validateClientStats(
+                "10101,1,2,3,0,0,0,0,3,1\n"
+                        + "10101,2,3,4,0,0,0,0,2,1\n"
+                        + "10101,3,4,5,0,0,0,0,1,1");
+    }
+
+    @Test
+    public void testLogPacketDropped_MultipleClients() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketDropped(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketDropped(LAYER2, 1);
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "10101,1,2,3,0,0,0,0,3,1\n"
+                        + "10102,1,2,3,0,0,0,0,2,1\n"
+                        + "10102,2,3,4,0,0,0,0,1,1\n");
+    }
+
+    @Test
+    public void testLogPackets_MultipleClients_MultipleLayers_MultipleOperations() {
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketSent(LAYER, 3);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID)
+                .logPacketDropped(LAYER, 1);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketReceived(LAYER, 2);
+        mCarStatsService.getVmsClientLog(CLIENT_UID2)
+                .logPacketSent(LAYER2, 2);
+        mCarStatsService.getVmsClientLog(-1)
+                .logPacketDropped(LAYER2, 12);
+
+
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n"
+                        + "10102,test.package2,0,0,0,0,0\n",
+                "-1,2,3,4,0,0,0,0,12,1\n"
+                        + "10101,1,2,3,3,1,2,1,1,1\n"
+                        + "10102,1,2,3,0,0,6,3,0,0\n"
+                        + "10102,2,3,4,2,1,0,0,0,0\n");
+    }
+
+
+    private void validateConnectionStats(String vmsConnectionStats) {
+        validateDumpsys(vmsConnectionStats + "\n", "");
+    }
+
+    private void validateClientStats(String vmsClientStats) {
+        validateDumpsys(
+                "10101,test.package,0,0,0,0,0\n",
+                vmsClientStats + "\n");
+    }
+
+    private void validateDumpsys(String vmsConnectionStats, String vmsClientStats) {
+        mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+        assertEquals(
+                "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+                        + vmsConnectionStats
+                        + "\n"
+                        + "uid,layerType,layerChannel,layerVersion,"
+                        + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n"
+                        + vmsClientStats,
+                mDumpsysOutput.toString());
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
index dc89528..87bf8b1 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -50,12 +50,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 
@@ -63,6 +65,9 @@
 
 import com.android.car.VmsPublisherService;
 import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLog;
+import com.android.car.stats.VmsClientLog.ConnectionState;
 import com.android.car.user.CarUserService;
 import com.android.car.user.CarUserService.UserCallback;
 
@@ -98,6 +103,11 @@
     private static final String HAL_CLIENT_NAME = "HalClient";
     private static final String UNKNOWN_PACKAGE = "UnknownPackage";
 
+    private static final int TEST_APP_ID = 12345;
+    private static final int TEST_SYSTEM_UID = 12345;
+    private static final int TEST_USER_UID = 1012345;
+    private static final int TEST_USER_UID_U11 = 1112345;
+
     private static final long MILLIS_BEFORE_REBIND = 100;
 
     @Mock
@@ -106,7 +116,8 @@
     private PackageManager mPackageManager;
     @Mock
     private Resources mResources;
-
+    @Mock
+    private CarStatsService mStatsService;
     @Mock
     private UserManager mUserManager;
     @Mock
@@ -145,11 +156,23 @@
 
     private UserCallback mUserCallback;
     private MockitoSession mSession;
+    @Mock
+    private VmsClientLog mSystemClientLog;
+    @Mock
+    private VmsClientLog mUserClientLog;
+    @Mock
+    private VmsClientLog mUserClientLog2;
+    @Mock
+    private VmsClientLog mHalClientLog;
+
     private VmsClientManager mClientManager;
 
     private int mForegroundUserId;
     private int mCallingAppUid;
 
+    private ServiceInfo mSystemServiceInfo;
+    private ServiceInfo mUserServiceInfo;
+
     @Before
     public void setUp() throws Exception {
         mSession = mockitoSession()
@@ -159,9 +182,24 @@
                 .startMocking();
 
         resetContext();
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
-        when(mPackageManager.getServiceInfo(any(), anyInt())).thenReturn(serviceInfo);
+        mSystemServiceInfo = new ServiceInfo();
+        mSystemServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+        mSystemServiceInfo.applicationInfo = new ApplicationInfo();
+        mSystemServiceInfo.applicationInfo.uid = TEST_APP_ID;
+        when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
+                .thenReturn(mSystemServiceInfo);
+        when(mStatsService.getVmsClientLog(TEST_SYSTEM_UID)).thenReturn(mSystemClientLog);
+
+        mUserServiceInfo = new ServiceInfo();
+        mUserServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+        mUserServiceInfo.applicationInfo = new ApplicationInfo();
+        mUserServiceInfo.applicationInfo.uid = TEST_APP_ID;
+        when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
+                .thenReturn(mUserServiceInfo);
+        when(mStatsService.getVmsClientLog(TEST_USER_UID)).thenReturn(mUserClientLog);
+        when(mStatsService.getVmsClientLog(TEST_USER_UID_U11)).thenReturn(mUserClientLog2);
+
+        when(mStatsService.getVmsClientLog(Process.myUid())).thenReturn(mHalClientLog);
 
         when(mResources.getInteger(
                 com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher)).thenReturn(
@@ -179,8 +217,8 @@
         mForegroundUserId = USER_ID;
         mCallingAppUid = UserHandle.getUid(USER_ID, 0);
 
-        mClientManager = new VmsClientManager(mContext, mBrokerService, mUserService, mHal,
-                mHandler, () -> mCallingAppUid);
+        mClientManager = new VmsClientManager(mContext, mStatsService, mUserService,
+                mBrokerService, mHal, mHandler, () -> mCallingAppUid);
         verify(mHal).setClientManager(mClientManager);
         mClientManager.setPublisherService(mPublisherService);
 
@@ -196,6 +234,7 @@
         verify(mContext, atLeast(0)).getResources();
         verify(mContext, atLeast(0)).getPackageManager();
         verifyNoMoreInteractions(mContext, mBrokerService, mHal, mPublisherService, mHandler);
+        verifyNoMoreInteractions(mSystemClientLog, mUserClientLog, mUserClientLog2, mHalClientLog);
         mSession.finishMocking();
     }
 
@@ -238,14 +277,12 @@
 
     @Test
     public void testSystemUserUnlocked_WrongPermission() throws Exception {
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
-        when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
-                .thenReturn(serviceInfo);
+        mSystemServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
         notifySystemUserUnlocked();
 
         // Process will not be bound
         verifySystemBind(0);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -256,6 +293,7 @@
 
         // Failure state will trigger another attempt on event
         verifySystemBind(2);
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -267,6 +305,7 @@
 
         // Failure state will trigger another attempt on event
         verifySystemBind(2);
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -298,14 +337,12 @@
 
     @Test
     public void testUserUnlocked_WrongPermission() throws Exception {
-        ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
-        when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
-                .thenReturn(serviceInfo);
+        mUserServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
         notifyUserUnlocked(USER_ID, true);
 
         // Process will not be bound
         verifyUserBind(0);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -317,6 +354,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -328,6 +366,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -339,6 +378,7 @@
 
         // Failure state will trigger another attempt
         verifyUserBind(2);
+        verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
     }
 
     @Test
@@ -347,6 +387,7 @@
                 .thenReturn(false);
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -362,6 +403,7 @@
                 .thenReturn(false);
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -370,6 +412,7 @@
         notifyUserUnlocked(USER_ID, true);
 
         verifySystemBind(2); // Failure state will trigger another attempt
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
         verifyUserBind(1);
     }
 
@@ -379,6 +422,7 @@
                 .thenThrow(new SecurityException());
         notifySystemUserUnlocked();
         verifySystemBind(1);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
         resetContext();
 
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -387,6 +431,7 @@
         notifyUserUnlocked(USER_ID, true);
 
         verifySystemBind(2); // Failure state will trigger another attempt
+        verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
         verifyUserBind(1);
     }
 
@@ -429,6 +474,7 @@
     public void testOnSystemServiceConnected() {
         IBinder binder = bindSystemClient();
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
     }
 
     private IBinder bindSystemClient() {
@@ -446,6 +492,7 @@
     public void testOnUserServiceConnected() {
         IBinder binder = bindUserClient();
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
     }
 
     private IBinder bindUserClient() {
@@ -467,10 +514,12 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
@@ -487,14 +536,17 @@
         IBinder binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
-        reset(mPublisherService);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
+        reset(mPublisherService, mSystemClientLog);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
 
         verifyAndRunRebindTask();
         // No more interactions (verified by tearDown)
@@ -509,10 +561,12 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onBindingDied(null);
         verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+        verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
@@ -543,10 +597,12 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
@@ -563,14 +619,17 @@
         IBinder binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
-        reset(mPublisherService);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
+        reset(mPublisherService, mUserClientLog);
 
         connection.onServiceDisconnected(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         binder = createPublisherBinder();
         connection.onServiceConnected(null, binder);
         verifyOnClientConnected(USER_CLIENT_NAME, binder);
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
 
         verifyAndRunRebindTask();
         // No more interactions (verified by tearDown)
@@ -584,10 +643,12 @@
 
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         connection.onBindingDied(null);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
 
         verifyAndRunRebindTask();
         verify(mContext).unbindService(connection);
@@ -614,15 +675,18 @@
     public void testOnUserSwitched_UserChange() {
         notifyUserUnlocked(USER_ID, true);
         verifyUserBind(1);
+        resetContext();
+
         ServiceConnection connection = mConnectionCaptor.getValue();
         connection.onServiceConnected(null, createPublisherBinder());
-        resetContext();
+        verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         notifyUserSwitched(USER_ID_U11, true);
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(1);
     }
 
@@ -639,6 +703,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(0);
     }
 
@@ -655,6 +720,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(0);
     }
 
@@ -697,6 +763,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         verifyUserBind(1);
     }
 
@@ -715,6 +782,7 @@
 
         verify(mContext).unbindService(connection);
         verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+        verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
         // User processes will not be bound for system user
         verifyUserBind(0);
     }
@@ -736,7 +804,9 @@
     public void testAddSubscriber() {
         mClientManager.addSubscriber(mSubscriberClient1);
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -746,7 +816,9 @@
 
         mClientManager.addSubscriber(mSubscriberClient1);
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -760,6 +832,7 @@
             // expected
         }
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
@@ -768,7 +841,9 @@
         mClientManager.addSubscriber(mSubscriberClient1);
         verify(mPackageManager, atMost(1)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -777,11 +852,14 @@
         mClientManager.addSubscriber(mSubscriberClient2);
         verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
     public void testAddSubscriber_MultipleClients_ForegroundAndSystemUsers_SamePackage() {
+        int clientUid1 = mCallingAppUid;
         mClientManager.addSubscriber(mSubscriberClient1);
 
         mCallingAppUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 0);
@@ -790,12 +868,15 @@
 
         verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
 
     @Test
     public void testAddSubscriber_MultipleClients_MultiplePackages() {
+        int clientUid1 = mCallingAppUid;
         mClientManager.addSubscriber(mSubscriberClient1);
 
         mCallingAppUid = UserHandle.getUid(mForegroundUserId, 1);
@@ -804,7 +885,9 @@
 
         verify(mPackageManager, times(2)).getNameForUid(anyInt());
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals("test.package2", mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
     }
 
     @Test
@@ -813,12 +896,14 @@
         mClientManager.removeSubscriber(mSubscriberClient1);
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
     public void testRemoveSubscriber_NotRegistered() {
         mClientManager.removeSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
@@ -830,6 +915,7 @@
 
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
     }
 
     @Test
@@ -840,6 +926,7 @@
         mClientManager.switchUser();
         verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertTrue(mClientManager.getAllSubscribers().isEmpty());
     }
 
@@ -855,7 +942,10 @@
         when(mPackageManager.getNameForUid(mCallingAppUid)).thenReturn(TEST_PACKAGE);
         mClientManager.addSubscriber(mSubscriberClient2);
 
+        assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+        assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
         assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+        assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
         assertFalse(mClientManager.getAllSubscribers().contains(mSubscriberClient1));
         assertTrue(mClientManager.getAllSubscribers().contains(mSubscriberClient2));
     }
@@ -883,6 +973,7 @@
         IVmsPublisherClient publisherClient = createPublisherClient();
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         mForegroundUserId = USER_ID_U11;
@@ -898,6 +989,7 @@
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
         verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
     }
@@ -910,6 +1002,7 @@
 
         mClientManager.onHalConnected(publisherClient, subscriberClient);
         verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
     }
@@ -919,11 +1012,13 @@
         IVmsPublisherClient publisherClient = createPublisherClient();
         IVmsSubscriberClient subscriberClient = createSubscriberClient();
         mClientManager.onHalConnected(publisherClient, subscriberClient);
+        verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
         reset(mPublisherService);
 
         mClientManager.onHalDisconnected();
         verify(mPublisherService).onClientDisconnected(eq(HAL_CLIENT_NAME));
         verify(mBrokerService).removeDeadSubscriber(eq(subscriberClient));
+        verify(mHalClientLog).logConnectionState(ConnectionState.DISCONNECTED);
         assertFalse(mClientManager.getAllSubscribers().contains(subscriberClient));
         assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(subscriberClient));
     }
@@ -936,7 +1031,7 @@
     }
 
     private void resetContext() {
-        reset(mContext);
+        reset(mContext, mSystemClientLog, mUserClientLog);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(true);
         when(mContext.getResources()).thenReturn(mResources);
@@ -966,10 +1061,16 @@
     }
 
     private void verifySystemBind(int times) {
+        verify(mSystemClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
         verifyBind(times, SYSTEM_CLIENT_COMPONENT, UserHandle.SYSTEM);
     }
 
     private void verifyUserBind(int times) {
+        if (mForegroundUserId == USER_ID) {
+            verify(mUserClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
+        } else if (mForegroundUserId == USER_ID_U11) {
+            verify(mUserClientLog2, times(times)).logConnectionState(ConnectionState.CONNECTING);
+        }
         verifyBind(times, USER_CLIENT_COMPONENT, UserHandle.of(mForegroundUserId));
     }
 
diff --git a/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
new file mode 100755
index 0000000..8beb1f4
--- /dev/null
+++ b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+if [ -z "$ANDROID_PRODUCT_OUT" ]; then
+    echo "ANDROID_PRODUCT_OUT not set"
+    exit
+fi
+DISP_ID=1
+if [[ $# -eq 1 ]]; then
+  echo "$1"
+  DISP_ID=$1
+fi
+echo "Use display:$DISP_ID"
+
+adb root
+# Check always crashing one
+echo "Start AlwaysCrashingActivity in fixed mode"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.AlwaysCrashingActivity
+sleep 1
+read -p "AlwaysCrashingAvtivity should not be tried any more. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# suspend-resume
+echo "Check retry for suspend - resume"
+adb shell setprop android.car.garagemodeduration 1
+adb shell dumpsys car_service suspend
+adb shell dumpsys car_service resume
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# starting other Activity
+echo "Switch to no crash Activity"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.NoCrashActivity
+read -p "NoCrashAvtivity should have been shown. Press Enter"
+# stating other non-fixed Activity
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "NoCrashAvtivity should be shown after showing EmptyActivity. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "NoCrashActivity should be shown. Press Enter"
+# stop the mode
+echo "Stop fixed activity mode"
+adb shell dumpsys car_service stop-fixed-activity-mode $DISP_ID
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "EmptyActivity should be shown. Press Enter"