Added user lifecycle events to CarUserManager.

Test: atest CtsCarTestCases:android.car.cts.CarUserManagerTest
Bug: 144120654
Bug: 146207078

Change-Id: I1c4ca36337f68d893089b3c07b54ff5d02456e67
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index d84c798..e2f21b5 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -29,18 +29,21 @@
 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.car.ICarUserService;
 import android.car.settings.CarSettings;
+import android.car.user.CarUserManager;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.graphics.Bitmap;
 import android.location.LocationManager;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.util.Log;
+import android.util.SparseArray;
 import android.util.TimingsTraceLog;
 
 import com.android.car.CarServiceBase;
@@ -48,6 +51,7 @@
 import com.android.car.hal.UserHalService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.IResultReceiver;
 import com.android.internal.util.UserIcons;
 
 import java.io.PrintWriter;
@@ -69,6 +73,8 @@
  * <ol/>
  */
 public final class CarUserService extends ICarUserService.Stub implements CarServiceBase {
+
+
     private final Context mContext;
     private final CarUserManagerHelper mCarUserManagerHelper;
     private final IActivityManager mAm;
@@ -96,10 +102,18 @@
     @GuardedBy("mLockUser")
     private final ArrayList<Integer> mBackgroundUsersRestartedHere = new ArrayList<>();
 
+    // TODO(b/144120654): mege then
     private final CopyOnWriteArrayList<UserCallback> mUserCallbacks = new CopyOnWriteArrayList<>();
 
     private final UserHalService mHal;
 
+    /**
+     * List of lifecycle listeners by uid.
+     */
+    @GuardedBy("mLockUser")
+    private final SparseArray<IResultReceiver> mLifecycleListeners = new SparseArray<>();
+
+    // TODO(b/144120654): replace by CarUserManager listener
     /** Interface for callbacks related to user activities. */
     public interface UserCallback {
         /** Gets called when user lock status has been changed. */
@@ -170,7 +184,19 @@
     public void dump(@NonNull PrintWriter writer) {
         checkAtLeastOnePermission("dump()", android.Manifest.permission.DUMP);
         writer.println("*CarUserService*");
+        String indent = "  ";
         synchronized (mLockUser) {
+            int numberListeners = mLifecycleListeners.size();
+            if (numberListeners == 0) {
+                writer.println("No lifecycle listeners");
+            } else {
+                writer.printf("%d lifecycle listeners\n", numberListeners);
+                for (int i = 0; i < numberListeners; i++) {
+                    int uid = mLifecycleListeners.keyAt(i);
+                    IResultReceiver listener = mLifecycleListeners.valueAt(i);
+                    writer.printf("%suid: %d Listener %s\n", indent, uid, listener);
+                }
+            }
             writer.println("User0Unlocked: " + mUser0Unlocked);
             writer.println("MaxRunningUsers: " + mMaxRunningUsers);
             writer.println("BackgroundUsersToRestart: " + mBackgroundUsersToRestart);
@@ -178,10 +204,9 @@
             List<UserInfo> allDrivers = getAllDrivers();
             int driversSize = allDrivers.size();
             writer.println("NumberOfDrivers: " + driversSize);
-            String prefix = "  ";
             for (int i = 0; i < driversSize; i++) {
                 int driverId = allDrivers.get(i).id;
-                writer.print(prefix + "#" + i + ": id=" + driverId);
+                writer.print(indent + "#" + i + ": id=" + driverId);
                 List<UserInfo> passengers = getPassengers(driverId);
                 int passengersSize = passengers.size();
                 writer.print(" NumberPassengers: " + passengersSize);
@@ -405,6 +430,42 @@
         }
     }
 
+    @Override
+    public void setLifecycleListenerForUid(IResultReceiver listener) {
+        int uid = Binder.getCallingUid();
+        checkInteractAcrossUsersPermission("setLifecycleListenerForUid" + uid);
+
+        try {
+            listener.asBinder().linkToDeath(() -> onListenerDeath(uid), 0);
+        } catch (RemoteException e) {
+            Log.wtf(TAG_USER, "Cannot listen to death of " + uid);
+        }
+        synchronized (mLockUser) {
+            mLifecycleListeners.append(uid, listener);
+        }
+    }
+
+    private void onListenerDeath(int uid) {
+        Log.i(TAG_USER, "Removing listeners for uid " + uid + " on binder death");
+        synchronized (mLockUser) {
+            removeLifecycleListenerLocked(uid);
+        }
+    }
+
+    @Override
+    public void resetLifecycleListenerForUid() {
+        int uid = Binder.getCallingUid();
+        checkInteractAcrossUsersPermission("resetLifecycleListenerForUid-" + uid);
+
+        synchronized (mLockUser) {
+            removeLifecycleListenerLocked(uid);
+        }
+    }
+
+    private void removeLifecycleListenerLocked(int uid) {
+        mLifecycleListeners.remove(uid);
+    }
+
     /** Returns whether the given user is a system user. */
     private static boolean isSystemUser(@UserIdInt int userId) {
         return userId == UserHandle.USER_SYSTEM;
@@ -612,9 +673,10 @@
      * @param userId User id of new user.
      */
     public void onSwitchUser(@UserIdInt int userId) {
-        TimingsTraceLog t = new TimingsTraceLog(TAG_USER,
-                Trace.TRACE_TAG_SYSTEM_SERVER);
+        Log.i(TAG_USER, "onSwitchUser() callback for user " + userId);
+        TimingsTraceLog t = new TimingsTraceLog(TAG_USER, Trace.TRACE_TAG_SYSTEM_SERVER);
         t.traceBegin("onSwitchUser-" + userId);
+
         if (!isSystemUser(userId)) {
             mCarUserManagerHelper.setLastActiveUser(userId);
         }
@@ -625,12 +687,52 @@
             setupPassengerUser();
             startFirstPassenger(userId);
         }
+
+        // TODO(b/144120654): right now just the app listeners are running in the background so the
+        // CTS tests pass (as otherwise they might fail if a car service callback takes too long),
+        // but once we refactor the car service callback into lifecycle listeners, we should use a
+        // proper thread management (like a Threadpool / executor);
+
+        int listenersSize = mLifecycleListeners.size();
+        if (listenersSize == 0) {
+            Log.i(TAG_USER, "Not notifying app listeners");
+        } else {
+            new Thread(() -> {
+                // Must use a different TimingsTraceLog because it's another thread
+                TimingsTraceLog t2 = new TimingsTraceLog(TAG_USER, Trace.TRACE_TAG_SYSTEM_SERVER);
+                Log.i(TAG_USER, "Notifying " + listenersSize + " listeners");
+                for (int i = 0; i < listenersSize; i++) {
+                    int uid = mLifecycleListeners.keyAt(i);
+                    IResultReceiver listener = mLifecycleListeners.valueAt(i);
+                    t2.traceBegin("notify-listener-" + uid);
+                    Bundle data = new Bundle();
+                    data.putInt(CarUserManager.BUNDLE_PARAM_ACTION,
+                            CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING);
+                    // TODO(b/144120654): should pass currentId from CarServiceHelperService so it
+                    // can set BUNDLE_PARAM_PREVIOUS_USER_HANDLE
+                    if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+                        Log.d(TAG_USER, "Notifying listener for uid " + uid);
+                    }
+                    try {
+                        listener.send(userId, data);
+                    } catch (RemoteException e) {
+                        Log.e(TAG_USER, "Error calling lifecycle listener", e);
+                    } finally {
+                        t2.traceEnd();
+                    }
+                }
+
+            }, "SwitchUser-" + userId + "-Listeners").start();
+        }
+
+        Log.i(TAG_USER, "Notifying " + mUserCallbacks.size() + " callbacks");
         for (UserCallback callback : mUserCallbacks) {
-            t.traceBegin("onSwitchUser-" + callback.getClass().getSimpleName());
+            t.traceBegin("onSwitchUser-" + callback.getClass().getName());
             callback.onSwitchUser(userId);
             t.traceEnd();
         }
-        t.traceEnd();
+        t.traceEnd(); // onSwitchUser
+
     }
 
     /**
@@ -746,11 +848,16 @@
                 android.Manifest.permission.DUMP);
     }
 
+    private void checkInteractAcrossUsersPermission(String message) {
+        checkAtLeastOnePermission(message, android.Manifest.permission.INTERACT_ACROSS_USERS,
+                android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+    }
+
     private static void checkAtLeastOnePermission(String message, String...permissions) {
         int callingUid = Binder.getCallingUid();
         if (!hasAtLeastOnePermissionGranted(callingUid, permissions)) {
             throw new SecurityException("You need one of " + Arrays.toString(permissions)
-            + " to: " + message);
+                    + " to: " + message);
         }
     }