Added option to dump end-to-end duration of first unlocked user.

This will be useful to measure the impact of using the User HAL to get
the initial user.

Test: atest CarServicesTest:com.android.internal.car.CarHelperServiceTest \
            CarServiceUnitTest:com.android.car.user.CarUserServiceTest
Test: m android.car.testapi
Test: adb shell dumpsys car_service --first-user-metrics

Bug: 141388849
Bug: 146207078
Bug: 150222501

Change-Id: Iaad06d3cd2c72061650c27a14cba7f16c1d809bc
diff --git a/car-lib/src/android/car/ICar.aidl b/car-lib/src/android/car/ICar.aidl
index 0b0ae11..a62d8fa 100644
--- a/car-lib/src/android/car/ICar.aidl
+++ b/car-lib/src/android/car/ICar.aidl
@@ -19,13 +19,11 @@
 /** @hide */
 interface ICar {
     // All oneway methods are called from system server and should be placed in top positions.
-    // Do not change the order of oneway methods as system server make binder call based on this
-    // order.
+    // Do not change the number of oneway methods as system server make binder call based on this
+    // order - if you change them, you need to change the constants on CarServiceHelperService.
 
     /**
      * IBinder is ICarServiceHelper but passed as IBinder due to aidl hidden.
-     *
-     * This should be the 1st method. Do not change the order.
      */
     oneway void setCarServiceHelper(in IBinder helper) = 0;
 
@@ -36,29 +34,36 @@
      * @param timestampMs - when the event happened
      * @param fromUserId - user id of previous user when type is SWITCHING (or UserHandle.USER_NULL)
      * @param toUserId - user id of new user.
-     *
-     * This should be the 2nd method. Do not change the order.
      */
     oneway void onUserLifecycleEvent(int eventType, long timestampMs, int fromUserId,
             int toUserId) = 1;
 
     /**
+     * Notify when first user was unlocked, for metrics (and lifecycle) purposes.
+     *
+     * @param userId - id of first non-system user locked
+     * @param timestampMs - when the user was unlocked
+     * @param duration - how long it took to unlock (from SystemServer start)
+     */
+    oneway void onFirstUserUnlocked(int userId, long timestampMs, long duration) = 2;
+
+
+    // TODO(b/145689885): 2 method below are deprecated (onUserLifecycleEvent covers then) so
+    // they're have higher codes to make it easier to add other
+
+    /**
      * Notify lock / unlock of user id to car service.
      * unlocked: 1 if unlocked 0 otherwise.
-     *
-     * This should be the 3rd method. Do not change the order.
      */
-    oneway void setUserLockStatus(in int userId, in int unlocked) = 2;
+    oneway void setUserLockStatus(in int userId, in int unlocked) = 9;
 
     /**
      * Notify of user switching.  This is called only for foreground users when the user is starting
      * to boot.
      *
      * @param userId - user id of new user.
-     *
-     * This should be the 4th method. Do not change the order.
      */
-    oneway void onSwitchUser(in int userId) = 3;
+    oneway void onSwitchUser(in int userId) = 10;
 
 
     // Methods below start on 11 to make it easier to add more oneway methods above
diff --git a/car-test-lib/src/android/car/testapi/FakeCar.java b/car-test-lib/src/android/car/testapi/FakeCar.java
index 932ac76..b6f22fb 100644
--- a/car-test-lib/src/android/car/testapi/FakeCar.java
+++ b/car-test-lib/src/android/car/testapi/FakeCar.java
@@ -167,6 +167,11 @@
         }
 
         @Override
+        public void onFirstUserUnlocked(int userId, long timestampMs, long duration) {
+            // Nothing to do yet.
+        }
+
+        @Override
         public void setUserLockStatus(int userHandle, int unlocked) throws RemoteException {
             // Nothing to do yet.
         }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index a68e171..0ff4bc0 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -366,6 +366,11 @@
     }
 
     @Override
+    public void onFirstUserUnlocked(int userId, long timestampMs, long duration) {
+        mUserMetrics.logFirstUnlockedUser(userId, timestampMs, duration);
+    }
+
+    @Override
     public boolean isFeatureEnabled(String featureName) {
         return mFeatureController.isFeatureEnabled(featureName);
     }
@@ -670,6 +675,8 @@
             return;
         } else if ("--user-metrics".equals(args[0])) {
             mUserMetrics.dump(writer);
+        } else if ("--first-user-metrics".equals(args[0])) {
+            mUserMetrics.dumpFirstUserUnlockDuration(writer);
         } else if ("--help".equals(args[0])) {
             showDumpHelp(writer);
         } else if (Build.IS_USERDEBUG || Build.IS_ENG) {
@@ -711,6 +718,9 @@
         writer.println("\t  where HAL is just the class name (like UserHalService)");
         writer.println("--user-metrics");
         writer.println("\t  dumps user switching and stopping metrics ");
+        writer.println("--first-user-metrics");
+        writer.println("\t  dumps how long it took to unlock first user since Android started\n");
+        writer.println("\t  (or -1 if not unlocked)");
         writer.println("-h");
         writer.println("\t  shows commands usage (NOTE: commands are not available on USER builds");
         writer.println("[ANYTHING ELSE]");
diff --git a/service/src/com/android/car/user/UserMetrics.java b/service/src/com/android/car/user/UserMetrics.java
index dad71fd..6c94f28 100644
--- a/service/src/com/android/car/user/UserMetrics.java
+++ b/service/src/com/android/car/user/UserMetrics.java
@@ -31,6 +31,7 @@
 import android.util.LocalLog;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseLongArray;
 import android.util.TimeUtils;
 
 import com.android.internal.annotations.GuardedBy;
@@ -78,6 +79,9 @@
     @GuardedBy("mLock")
     private final LocalLog mUserStoppedLogs = new LocalLog(LOG_SIZE);
 
+    @GuardedBy("mLock")
+    private final SparseLongArray mFirstUserUnlockDuration = new SparseLongArray(1);
+
     /**
      * Logs a user lifecycle event.
      */
@@ -109,6 +113,16 @@
         }
     }
 
+    /**
+     * Logs when the first user was unlocked.
+     */
+    public void logFirstUnlockedUser(int userId, long timestampMs, long duration) {
+        synchronized (mLock) {
+            mFirstUserUnlockDuration.put(userId, duration);
+            onUserUnlockedEventLocked(timestampMs, userId);
+        }
+    }
+
     private void onUserStartingEventLocked(long timestampMs, @UserIdInt int userId) {
         if (mUserStartingMetrics == null) {
             mUserStartingMetrics = new SparseArray<>(INITIAL_CAPACITY);
@@ -209,6 +223,14 @@
         pw.println("* User Metrics *");
         synchronized (mLock) {
 
+            if (mFirstUserUnlockDuration.size() == 0) {
+                pw.println("First user not unlocked yet");
+            } else {
+                pw.printf("First user (%d) unlocked in ", mFirstUserUnlockDuration.keyAt(0));
+                TimeUtils.formatDuration(mFirstUserUnlockDuration.valueAt(0), pw);
+                pw.println();
+            }
+
             dump(pw, "starting", mUserStartingMetrics);
             dump(pw, "stopping", mUserStoppingMetrics);
 
@@ -220,6 +242,19 @@
         }
     }
 
+    /**
+     * Dumps only how long it took to unlock the first user (or {@code -1} if not available).
+     */
+    public void dumpFirstUserUnlockDuration(@NonNull PrintWriter pw) {
+        synchronized (mLock) {
+            if (mFirstUserUnlockDuration.size() == 0) {
+                pw.println(-1);
+                return;
+            }
+            pw.println(mFirstUserUnlockDuration.valueAt(0));
+        }
+    }
+
     private void dump(@NonNull PrintWriter pw, @NonNull String message,
             @NonNull SparseArray<? extends BaseUserMetric> metrics) {
         String indent = "  ";
diff --git a/tests/carservice_unit_test/src/android/car/CarTest.java b/tests/carservice_unit_test/src/android/car/CarTest.java
index 1da65ff..ddb18c7 100644
--- a/tests/carservice_unit_test/src/android/car/CarTest.java
+++ b/tests/carservice_unit_test/src/android/car/CarTest.java
@@ -78,6 +78,10 @@
         }
 
         @Override
+        public void onFirstUserUnlocked(int userId, long timestampMs, long duration) {
+        }
+
+        @Override
         public void setUserLockStatus(int userHandle, int unlocked) {
         }