Deprecating UserCallback interface.

This commit corresponds to steps 1 and 2 from b/145689885 #16

Bug: 145689885
Test: atest CarServiceUnitTest
Change-Id: Ie7e1b2e852031098fe3767300ece7e9573341631
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 3858e9b..6546dd4 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -1240,7 +1240,7 @@
     field public static final int USER_LIFECYCLE_EVENT_TYPE_UNLOCKING = 3; // 0x3
   }
 
-  public final class CarUserManager.UserLifecycleEvent {
+  public static final class CarUserManager.UserLifecycleEvent {
     method public int getEventType();
     method @Nullable public android.os.UserHandle getPreviousUserHandle();
     method @NonNull public android.os.UserHandle getUserHandle();
diff --git a/car-lib/api/test-current.txt b/car-lib/api/test-current.txt
index 09d9fd7..f655eca 100644
--- a/car-lib/api/test-current.txt
+++ b/car-lib/api/test-current.txt
@@ -65,7 +65,7 @@
     field public static final int USER_LIFECYCLE_EVENT_TYPE_UNLOCKING = 3; // 0x3
   }
 
-  public final class CarUserManager.UserLifecycleEvent {
+  public static final class CarUserManager.UserLifecycleEvent {
     method public int getEventType();
     method @Nullable public android.os.UserHandle getPreviousUserHandle();
     method @NonNull public android.os.UserHandle getUserHandle();
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index 810373b..38b1958 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -482,12 +482,13 @@
      */
     @SystemApi
     @TestApi
-    public final class UserLifecycleEvent {
+    public static final class UserLifecycleEvent {
         private final @UserLifecycleEventType int mEventType;
         private final @NonNull UserHandle mUserHandle;
         private final @Nullable UserHandle mPreviousUserHandle;
 
-        private UserLifecycleEvent(@UserLifecycleEventType int eventType,
+        /** @hide */
+        public UserLifecycleEvent(@UserLifecycleEventType int eventType,
                 @NonNull UserHandle from, @Nullable UserHandle to) {
             mEventType = eventType;
             mPreviousUserHandle = from;
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index fedae06..164ecb8 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -30,6 +30,8 @@
 import android.car.ICarUserService;
 import android.car.settings.CarSettings;
 import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleEvent;
+import android.car.user.CarUserManager.UserLifecycleListener;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.content.pm.UserInfo;
@@ -113,19 +115,30 @@
     @GuardedBy("mLockUser")
     private final ArrayList<Integer> mBackgroundUsersRestartedHere = new ArrayList<>();
 
-    // TODO(b/144120654): mege then
+    // TODO(b/144120654): merge then
     private final CopyOnWriteArrayList<UserCallback> mUserCallbacks = new CopyOnWriteArrayList<>();
 
     private final UserHalService mHal;
 
     /**
+     * List of listeners to be notified on new user activities events.
+     */
+    private final CopyOnWriteArrayList<UserLifecycleListener>
+            mUserLifecycleListeners = new CopyOnWriteArrayList<>();
+
+    /**
      * 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. */
+    /**
+     * Interface for callbacks related to user activities.
+     *
+     * @deprecated {@link UserCallback} will be fully replaced by
+     *             {@link UserLifecycleListener} as part of b/145689885
+     */
+    @Deprecated
     public interface UserCallback {
         /** Gets called when user lock status has been changed. */
         void onUserLockChanged(@UserIdInt int userId, boolean unlocked);
@@ -567,18 +580,46 @@
         return !mUserManager.getUserInfo(userId).isEphemeral();
     }
 
-    /** Adds callback to listen to user activity events. */
+    /**
+     * Adds a new callback to listen to user activity events.
+     *
+     * @deprecated users should rely on {@link UserLifecycleListener} and invoke
+     *             {@link #addUserLifecycleListener} instead
+     */
+    @Deprecated
     public void addUserCallback(@NonNull UserCallback callback) {
         Objects.requireNonNull(callback, "callback cannot be null");
         mUserCallbacks.add(callback);
     }
 
-    /** Removes previously added callback to listen user events. */
+    /**
+     * Removes previously added user callback.
+     *
+     * @deprecated users should rely on {@link UserLifecycleListener} and invoke
+     *             {@link CarUserService#remove]UserLifecycleListener} instead
+     */
+    @Deprecated
     public void removeUserCallback(@NonNull UserCallback callback) {
         Objects.requireNonNull(callback, "callback cannot be null");
         mUserCallbacks.remove(callback);
     }
 
+    /**
+     * Adds a new {@link UserLifecycleListener} to listen to user activity events.
+     */
+    public void addUserLifecycleListener(@NonNull UserLifecycleListener listener) {
+        Objects.requireNonNull(listener, "listener cannot be null");
+        mUserLifecycleListeners.add(listener);
+    }
+
+    /**
+     * Removes previously added {@link UserLifecycleListener}.
+     */
+    public void removeUserLifecycleListener(@NonNull UserLifecycleListener listener) {
+        Objects.requireNonNull(listener, "listener cannot be null");
+        mUserLifecycleListeners.remove(listener);
+    }
+
     /** Adds callback to listen to passenger activity events. */
     public void addPassengerCallback(@NonNull PassengerCallback callback) {
         Objects.requireNonNull(callback, "callback cannot be null");
@@ -802,14 +843,43 @@
             }, "SwitchUser-" + userId + "-Listeners").start();
         }
 
-        Log.i(TAG_USER, "Notifying " + mUserCallbacks.size() + " callbacks");
+        notifyUserLifecycleListeners(t, userId);
+        notifyCallbacks(t, userId);
+    }
+
+    private void notifyUserLifecycleListeners(TimingsTraceLog t,
+            @UserIdInt int userId) {
+        if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+            Log.d(TAG_USER, "Notifying " + mUserLifecycleListeners.size()
+                    + " user lifecycle listeners");
+        }
+        // TODO(b/145689885): passing null for `from` parameter until it gets properly replaced
+        //     the expected Binder call.
+        UserLifecycleEvent event = new UserLifecycleEvent(
+                /* eventType= */ CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
+                /* from= */ null, /* to= */ new UserHandle(userId));
+        for (UserLifecycleListener listener : mUserLifecycleListeners) {
+            t.traceBegin("onEvent-" + listener.getClass().getName());
+            try {
+                listener.onEvent(event);
+            } catch (RuntimeException e) {
+                Log.e(TAG_USER,
+                        "Exception raised when invoking onEvent for " + listener, e);
+            }
+            t.traceEnd();
+        }
+    }
+
+    private void notifyCallbacks(TimingsTraceLog t, @UserIdInt int userId) {
+        if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+            Log.d(TAG_USER, "Notifying " + mUserCallbacks.size() + " callbacks");
+        }
         for (UserCallback callback : mUserCallbacks) {
             t.traceBegin("onSwitchUser-" + callback.getClass().getName());
             callback.onSwitchUser(userId);
             t.traceEnd();
         }
         t.traceEnd(); // onSwitchUser
-
     }
 
     /**
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
index 48fa261..ba77782 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
@@ -33,6 +33,8 @@
 import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -45,6 +47,9 @@
 import android.car.CarOccupantZoneManager.OccupantTypeEnum;
 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.car.settings.CarSettings;
+import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleEvent;
+import android.car.user.CarUserManager.UserLifecycleListener;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.content.pm.UserInfo;
@@ -77,6 +82,8 @@
 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.MockitoSession;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -117,6 +124,8 @@
     @Mock private UserManager mMockedUserManager;
     @Mock private Resources mMockedResources;
     @Mock private Drawable mMockedDrawable;
+    @Mock private UserLifecycleListener mUserLifecycleListener;
+    @Captor private ArgumentCaptor<UserLifecycleEvent> mArgumentCaptor;
 
     private MockitoSession mSession;
     private CarUserService mCarUserService;
@@ -201,6 +210,60 @@
                         UserHandle.of(UserHandle.USER_SYSTEM));
     }
 
+    @Test
+    public void testAddUserLifecycleListener_checkNullParameter() {
+        assertThrows(NullPointerException.class,
+                () -> mCarUserService.addUserLifecycleListener(null));
+    }
+
+    @Test
+    public void testRemoveUserLifecycleListener_checkNullParameter() {
+        assertThrows(NullPointerException.class,
+                () -> mCarUserService.removeUserLifecycleListener(null));
+    }
+
+    @Test
+    public void testOnSwitchUser_addListenerAndReceiveEvent() {
+        // Arrange
+        mCarUserService.addUserLifecycleListener(mUserLifecycleListener);
+
+        // Act
+        int anyNewUserId = 11;
+        mCarUserService.onSwitchUser(anyNewUserId);
+
+        // Verify
+        verifyListenerOnEventInvoked(anyNewUserId,
+                CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING);
+    }
+
+    @Test
+    public void testOnSwitchUser_ensureAllListenersAreNotified() {
+        // Arrange: add two listeners, one to fail on onEvent
+        // Adding the failure listener first.
+        UserLifecycleListener failureListener = mock(UserLifecycleListener.class);
+        doThrow(new RuntimeException("Failed onEvent invocation")).when(
+                failureListener).onEvent(any(UserLifecycleEvent.class));
+        mCarUserService.addUserLifecycleListener(failureListener);
+
+        // Adding the non-failure listener later.
+        mCarUserService.addUserLifecycleListener(mUserLifecycleListener);
+
+        // Act
+        int anyNewUserId = 11;
+        mCarUserService.onSwitchUser(anyNewUserId);
+
+        // Verify
+        verifyListenerOnEventInvoked(anyNewUserId,
+                CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING);
+    }
+
+    private void verifyListenerOnEventInvoked(int expectedNewUserId, int expectedEventType) {
+        verify(mUserLifecycleListener).onEvent(mArgumentCaptor.capture());
+        UserLifecycleEvent actualEvent = mArgumentCaptor.getValue();
+        assertThat(actualEvent.getEventType()).isEqualTo(expectedEventType);
+        assertThat(actualEvent.getUserHandle().getIdentifier()).isEqualTo(expectedNewUserId);
+    }
+
     /**
      * Test that the {@link CarUserService} disables the location service for headless user 0 upon
      * first run.