diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index d310d6e7530d7..834867cab3a85 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -799,6 +799,10 @@ public class Handler { * Remove any pending posts of messages with code 'what' and whose obj is * 'object' that are in the message queue. If object is null, * all messages will be removed. + *

+ * Similar to {@link #removeMessages(int, Object)} but uses object equality + * ({@link Object#equals(Object)}) instead of reference equality (==) in + * determining whether object is the message's obj'. * *@hide */ diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java index 99232746cea16..ed545a6f720c6 100644 --- a/services/core/java/com/android/server/SystemService.java +++ b/services/core/java/com/android/server/SystemService.java @@ -226,6 +226,76 @@ public abstract class SystemService { } } + /** + * Class representing the types of "onUser" events that we are being informed about as having + * finished. + * + * @hide + */ + public static final class UserCompletedEventType { + /** + * Flag representing the {@link #onUserStarting} event. + * @hide + */ + public static final int EVENT_TYPE_USER_STARTING = 1 << 0; + /** + * Flag representing the {@link #onUserUnlocked} event. + * @hide + */ + public static final int EVENT_TYPE_USER_UNLOCKED = 1 << 1; + /** + * Flag representing the {@link #onUserSwitching} event. + * @hide + */ + public static final int EVENT_TYPE_USER_SWITCHING = 1 << 2; + + /** + * @hide + */ + @IntDef(flag = true, prefix = "EVENT_TYPE_USER_", value = { + EVENT_TYPE_USER_STARTING, + EVENT_TYPE_USER_UNLOCKED, + EVENT_TYPE_USER_SWITCHING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EventTypesFlag { + } + + private @EventTypesFlag int mEventType; + + /** @hide */ + UserCompletedEventType(@EventTypesFlag int eventType) { + mEventType = eventType; + } + + /** Returns whether one of the events is {@link #onUserStarting}. */ + public boolean includesOnUserStarting() { + return (mEventType & EVENT_TYPE_USER_STARTING) != 0; + } + + /** Returns whether one of the events is {@link #onUserUnlocked}. */ + public boolean includesOnUserUnlocked() { + return (mEventType & EVENT_TYPE_USER_UNLOCKED) != 0; + } + + /** Returns whether one of the events is {@link #onUserSwitching}. */ + public boolean includesOnUserSwitching() { + return (mEventType & EVENT_TYPE_USER_SWITCHING) != 0; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + // List each in reverse order (to line up with binary better). + if (includesOnUserSwitching()) sb.append("|Switching"); + if (includesOnUserUnlocked()) sb.append("|Unlocked"); + if (includesOnUserStarting()) sb.append("|Starting"); + if (sb.length() > 1) sb.append("|"); + sb.append("}"); + return sb.toString(); + } + } + /** * Initializes the system service. *

@@ -406,6 +476,33 @@ public abstract class SystemService { public void onUserStopped(@NonNull TargetUser user) { } + /** + * Called some time after an onUser... event has completed, for the events delineated in + * {@link UserCompletedEventType}. May include more than one event. + * + *

+ * This can be useful for processing tasks that must run after such an event but are non-urgent. + * + * There are no strict guarantees about how long after the event this will be called, only that + * it will be called if applicable. There is no guarantee about the order in which each service + * is informed, and these calls may be made in parallel using a thread pool. + * + *

Note that if the event is no longer applicable (for example, we switched to user 10, but + * before this method was called, we switched to user 11), the event will not be included in the + * {@code eventType} (i.e. user 10 won't mention the switch - even though it happened, it is no + * longer applicable). + * + *

This method is only called when the service {@link #isUserSupported(TargetUser) supports} + * this user. + * + * @param user target user completing the event (e.g. user being switched to) + * @param eventType the types of onUser event applicable (e.g. user starting and being unlocked) + * + * @hide + */ + public void onUserCompletedEvent(@NonNull TargetUser user, UserCompletedEventType eventType) { + } + /** * Publish the service so it is accessible to other services and apps. * diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index e7f4de2a05883..0bc3fcc83d47c 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -36,6 +36,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.SystemServerClassLoaderFactory; import com.android.internal.util.Preconditions; import com.android.server.SystemService.TargetUser; +import com.android.server.SystemService.UserCompletedEventType; import com.android.server.am.EventLogTags; import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; @@ -73,6 +74,7 @@ public final class SystemServiceManager implements Dumpable { private static final String USER_SWITCHING = "Switch"; // Logged as onSwitchUser private static final String USER_STOPPING = "Stop"; // Logged as onStopUser private static final String USER_STOPPED = "Cleanup"; // Logged as onCleanupUser + private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onCompletedEventUser // Whether to use multiple threads to run user lifecycle phases in parallel. private static boolean sUseLifecycleThreadPool = true; @@ -404,6 +406,26 @@ public final class SystemServiceManager implements Dumpable { } } + /** + * Called some time after an onUser... event has completed, for the events delineated in + * {@link UserCompletedEventType}. + * + * @param eventFlags the events that completed, per {@link UserCompletedEventType}, or 0. + * @see SystemService#onUserCompletedEvent + */ + public void onUserCompletedEvent(@UserIdInt int userId, + @UserCompletedEventType.EventTypesFlag int eventFlags) { + EventLog.writeEvent(EventLogTags.SSM_USER_COMPLETED_EVENT, userId, eventFlags); + if (eventFlags == 0) { + return; + } + onUser(TimingsTraceAndSlog.newAsyncLog(), + USER_COMPLETED_EVENT, + /* prevUser= */ null, + getTargetUser(userId), + new UserCompletedEventType(eventFlags)); + } + private void onUser(@NonNull String onWhat, @UserIdInt int userId) { onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, getTargetUser(userId)); @@ -411,19 +433,23 @@ public final class SystemServiceManager implements Dumpable { private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, @Nullable TargetUser prevUser, @NonNull TargetUser curUser) { + onUser(t, onWhat, prevUser, curUser, /* completedEventType=*/ null); + } + + private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, + @Nullable TargetUser prevUser, @NonNull TargetUser curUser, + @Nullable UserCompletedEventType completedEventType) { final int curUserId = curUser.getUserIdentifier(); // NOTE: do not change label below, or it might break performance tests that rely on it. t.traceBegin("ssm." + onWhat + "User-" + curUserId); Slog.i(TAG, "Calling on" + onWhat + "User " + curUserId + (prevUser != null ? " (from " + prevUser + ")" : "")); - final int serviceLen = mServices.size(); - // Limit the lifecycle parallelization to all users other than the system user - // and only for the user start lifecycle phase for now. - final boolean useThreadPool = sUseLifecycleThreadPool - && curUserId != UserHandle.USER_SYSTEM - && onWhat.equals(USER_STARTING); + + final boolean useThreadPool = useThreadPool(curUserId, onWhat); final ExecutorService threadPool = useThreadPool ? Executors.newFixedThreadPool(mNumUserPoolThreads) : null; + + final int serviceLen = mServices.size(); for (int i = 0; i < serviceLen; i++) { final SystemService service = mServices.get(i); final String serviceName = service.getClass().getName(); @@ -446,8 +472,7 @@ public final class SystemServiceManager implements Dumpable { } continue; } - // Only submit this service to the thread pool if it's in the "other" category. - final boolean submitToThreadPool = useThreadPool && i >= sOtherServicesStartIndex; + final boolean submitToThreadPool = useThreadPool && useThreadPoolForService(onWhat, i); if (!submitToThreadPool) { t.traceBegin("ssm.on" + onWhat + "User-" + curUserId + "_" + serviceName); } @@ -459,7 +484,7 @@ public final class SystemServiceManager implements Dumpable { break; case USER_STARTING: if (submitToThreadPool) { - threadPool.submit(getOnStartUserRunnable(t, service, curUser)); + threadPool.submit(getOnUserStartingRunnable(t, service, curUser)); } else { service.onUserStarting(curUser); } @@ -476,6 +501,10 @@ public final class SystemServiceManager implements Dumpable { case USER_STOPPED: service.onUserStopped(curUser); break; + case USER_COMPLETED_EVENT: + threadPool.submit(getOnUserCompletedEventRunnable( + t, service, serviceName, curUser, completedEventType)); + break; default: throw new IllegalArgumentException(onWhat + " what?"); } @@ -498,9 +527,11 @@ public final class SystemServiceManager implements Dumpable { } catch (InterruptedException e) { Slog.wtf(TAG, "User lifecycle thread pool was interrupted while awaiting completion" + " of " + onWhat + " of user " + curUser, e); - Slog.e(TAG, "Couldn't terminate, disabling thread pool. " - + "Please capture a bug report."); - sUseLifecycleThreadPool = false; + if (!onWhat.equals(USER_COMPLETED_EVENT)) { + Slog.e(TAG, "Couldn't terminate, disabling thread pool. " + + "Please capture a bug report."); + sUseLifecycleThreadPool = false; + } } if (!terminated) { Slog.wtf(TAG, "User lifecycle thread pool was not terminated."); @@ -509,7 +540,38 @@ public final class SystemServiceManager implements Dumpable { t.traceEnd(); // main entry } - private Runnable getOnStartUserRunnable(TimingsTraceAndSlog oldTrace, SystemService service, + /** + * Whether the given onWhat should use a thread pool. + * IMPORTANT: changing the logic to return true won't necessarily make it multi-threaded. + * There needs to be a corresponding logic change in onUser() to actually submit + * to a threadPool for the given onWhat. + */ + private boolean useThreadPool(int userId, @NonNull String onWhat) { + switch (onWhat) { + case USER_STARTING: + // Limit the lifecycle parallelization to all users other than the system user + // and only for the user start lifecycle phase for now. + return sUseLifecycleThreadPool && userId != UserHandle.USER_SYSTEM; + case USER_COMPLETED_EVENT: + return true; + default: + return false; + } + } + + private boolean useThreadPoolForService(@NonNull String onWhat, int serviceIndex) { + switch (onWhat) { + case USER_STARTING: + // Only submit this service to the thread pool if it's in the "other" category. + return serviceIndex >= sOtherServicesStartIndex; + case USER_COMPLETED_EVENT: + return true; + default: + return false; + } + } + + private Runnable getOnUserStartingRunnable(TimingsTraceAndSlog oldTrace, SystemService service, TargetUser curUser) { return () -> { final TimingsTraceAndSlog t = new TimingsTraceAndSlog(oldTrace); @@ -531,6 +593,22 @@ public final class SystemServiceManager implements Dumpable { }; } + private Runnable getOnUserCompletedEventRunnable(TimingsTraceAndSlog oldTrace, + SystemService service, String serviceName, TargetUser curUser, + UserCompletedEventType eventType) { + return () -> { + final TimingsTraceAndSlog t = new TimingsTraceAndSlog(oldTrace); + final int curUserId = curUser.getUserIdentifier(); + t.traceBegin("ssm.on" + USER_COMPLETED_EVENT + "User-" + curUserId + + "_" + eventType + "_" + serviceName); + long time = SystemClock.elapsedRealtime(); + service.onUserCompletedEvent(curUser, eventType); + warnIfTooLong(SystemClock.elapsedRealtime() - time, service, + "on" + USER_COMPLETED_EVENT + "User-" + curUserId); + t.traceEnd(); + }; + } + /** Sets the safe mode flag for services to query. */ void setSafeMode(boolean safeMode) { mSafeMode = safeMode; diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index 68f2e35094fc8..b250a0cbae9eb 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -115,3 +115,4 @@ option java_package com.android.server.am 30085 ssm_user_unlocked (userId|1|5) 30086 ssm_user_stopping (userId|1|5) 30087 ssm_user_stopped (userId|1|5) +30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5) diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 252584c766962..e4c0846d090f7 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -109,6 +109,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.LockPatternUtils; import com.android.server.FgThread; import com.android.server.LocalServices; +import com.android.server.SystemService.UserCompletedEventType; import com.android.server.SystemServiceManager; import com.android.server.am.UserState.KeyEvictedCallback; import com.android.server.pm.UserManagerInternal; @@ -166,6 +167,7 @@ class UserController implements Handler.Callback { static final int REPORT_LOCKED_BOOT_COMPLETE_MSG = 110; static final int START_USER_SWITCH_FG_MSG = 120; static final int COMPLETE_USER_SWITCH_MSG = 130; + static final int USER_COMPLETED_EVENT_MSG = 140; // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not @@ -184,6 +186,14 @@ class UserController implements Handler.Callback { // when it never calls back. private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000; + /** + * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be + * scheduled (although it may fire sooner instead). + * When it fires, {@link #reportOnUserCompletedEvent} will be processed. + */ + // TODO(b/197344658): Increase to 10s or 15s once we have a switch-UX-is-done invocation too. + private static final int USER_COMPLETED_EVENT_DELAY_MS = 5 * 1000; + // Used for statsd logging with UserLifecycleJourneyReported + UserLifecycleEventOccurred atoms private static final long INVALID_SESSION_ID = 0; @@ -364,6 +374,13 @@ class UserController implements Handler.Callback { @GuardedBy("mUserIdToUserJourneyMap") private final SparseArray mUserIdToUserJourneyMap = new SparseArray<>(); + /** + * Map of userId to {@link UserCompletedEventType} event flags, indicating which as-yet- + * unreported user-starting events have transpired for the given user. + */ + @GuardedBy("mCompletedEventTypes") + private final SparseIntArray mCompletedEventTypes = new SparseIntArray(); + /** * Sets on {@link #setInitialConfig(boolean, int, boolean)}, which is called by * {@code ActivityManager} when the system is started. @@ -2778,6 +2795,9 @@ class UserController implements Handler.Callback { mInjector.getSystemServiceManager().onUserStarting( TimingsTraceAndSlog.newAsyncLog(), msg.arg1); + scheduleOnUserCompletedEvent(msg.arg1, + UserCompletedEventType.EVENT_TYPE_USER_STARTING, + USER_COMPLETED_EVENT_DELAY_MS); logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, USER_LIFECYCLE_EVENT_STATE_FINISH); @@ -2802,6 +2822,13 @@ class UserController implements Handler.Callback { break; case USER_UNLOCKED_MSG: mInjector.getSystemServiceManager().onUserUnlocked(msg.arg1); + scheduleOnUserCompletedEvent(msg.arg1, + UserCompletedEventType.EVENT_TYPE_USER_UNLOCKED, + // If it's the foreground user, we wait longer to let it fully load. + // Else, there's nothing specific to wait for, so we basically just proceed. + // (No need to acquire lock to read mCurrentUserId since it is volatile.) + // TODO: Find something to wait for in the case of a profile. + mCurrentUserId == msg.arg1 ? USER_COMPLETED_EVENT_DELAY_MS : 1000); logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKED_USER, USER_LIFECYCLE_EVENT_STATE_FINISH); clearSessionId(msg.arg1); @@ -2815,6 +2842,12 @@ class UserController implements Handler.Callback { Integer.toString(msg.arg1), msg.arg1); mInjector.getSystemServiceManager().onUserSwitching(msg.arg2, msg.arg1); + scheduleOnUserCompletedEvent(msg.arg1, + UserCompletedEventType.EVENT_TYPE_USER_SWITCHING, + USER_COMPLETED_EVENT_DELAY_MS); + break; + case USER_COMPLETED_EVENT_MSG: + reportOnUserCompletedEvent((Integer) msg.obj); break; case FOREGROUND_PROFILE_CHANGED_MSG: dispatchForegroundProfileChanged(msg.arg1); @@ -2846,6 +2879,71 @@ class UserController implements Handler.Callback { return false; } + /** + * Schedules {@link SystemServiceManager#onUserCompletedEvent()} with the given + * {@link UserCompletedEventType} event, which will be combined with any other events for that + * user already scheduled. + * If it isn't rescheduled first, it will fire after a delayMs delay. + * + * @param eventType event type flags from {@link UserCompletedEventType} to append to the + * schedule. Use 0 to schedule the ssm call without modifying the event types. + */ + // TODO(b/197344658): Also call scheduleOnUserCompletedEvent(userId, 0, 0) after switch UX done. + @VisibleForTesting + void scheduleOnUserCompletedEvent( + int userId, @UserCompletedEventType.EventTypesFlag int eventType, int delayMs) { + + if (eventType != 0) { + synchronized (mCompletedEventTypes) { + mCompletedEventTypes.put(userId, mCompletedEventTypes.get(userId, 0) | eventType); + } + } + + final Object msgObj = userId; + mHandler.removeEqualMessages(USER_COMPLETED_EVENT_MSG, msgObj); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(USER_COMPLETED_EVENT_MSG, msgObj), + delayMs); + } + + /** + * Calls {@link SystemServiceManager#onUserCompletedEvent()} for the given user, sending all the + * {@link UserCompletedEventType} events that have been scheduled for it if they are still + * applicable. + * + * Called on the mHandler thread. + */ + @VisibleForTesting + void reportOnUserCompletedEvent(Integer userId) { + mHandler.removeEqualMessages(USER_COMPLETED_EVENT_MSG, userId); + + int eventTypes; + synchronized (mCompletedEventTypes) { + eventTypes = mCompletedEventTypes.get(userId, 0); + mCompletedEventTypes.delete(userId); + } + + // Now, remove any eventTypes that are no longer true. + int eligibleEventTypes = 0; + synchronized (mLock) { + final UserState uss = mStartedUsers.get(userId); + if (uss != null && uss.state != UserState.STATE_SHUTDOWN) { + eligibleEventTypes |= UserCompletedEventType.EVENT_TYPE_USER_STARTING; + } + if (uss != null && uss.state == STATE_RUNNING_UNLOCKED) { + eligibleEventTypes |= UserCompletedEventType.EVENT_TYPE_USER_UNLOCKED; + } + if (userId == mCurrentUserId) { + eligibleEventTypes |= UserCompletedEventType.EVENT_TYPE_USER_SWITCHING; + } + } + Slogf.i(TAG, "reportOnUserCompletedEvent(%d): stored=%s, eligible=%s", userId, + Integer.toBinaryString(eventTypes), Integer.toBinaryString(eligibleEventTypes)); + eventTypes &= eligibleEventTypes; + + mInjector.systemServiceManagerOnUserCompletedEvent(userId, eventTypes); + } + /** * statsd helper method for logging the start of a user journey via a UserLifecycleEventOccurred * atom given the originating and targeting users for the journey. @@ -3086,7 +3184,11 @@ class UserController implements Handler.Callback { } void systemServiceManagerOnUserStopped(@UserIdInt int userId) { - mService.mSystemServiceManager.onUserStopped(userId); + getSystemServiceManager().onUserStopped(userId); + } + + void systemServiceManagerOnUserCompletedEvent(@UserIdInt int userId, int eventTypes) { + getSystemServiceManager().onUserCompletedEvent(userId, eventTypes); } protected UserManagerService getUserManager() { @@ -3113,7 +3215,7 @@ class UserController implements Handler.Callback { } boolean isRuntimeRestarted() { - return mService.mSystemServiceManager.isRuntimeRestarted(); + return getSystemServiceManager().isRuntimeRestarted(); } SystemServiceManager getSystemServiceManager() { diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 6818d1f6851ba..49635a99c84bc 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -33,6 +33,7 @@ import static com.android.server.am.UserController.CONTINUE_USER_SWITCH_MSG; import static com.android.server.am.UserController.REPORT_LOCKED_BOOT_COMPLETE_MSG; import static com.android.server.am.UserController.REPORT_USER_SWITCH_COMPLETE_MSG; import static com.android.server.am.UserController.REPORT_USER_SWITCH_MSG; +import static com.android.server.am.UserController.USER_COMPLETED_EVENT_MSG; import static com.android.server.am.UserController.USER_CURRENT_MSG; import static com.android.server.am.UserController.USER_START_MSG; import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG; @@ -45,6 +46,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.any; @@ -91,6 +93,7 @@ import android.util.Log; import androidx.test.filters.SmallTest; import com.android.server.FgThread; +import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; @@ -167,6 +170,8 @@ public class UserControllerTest { doReturn(false).when(mInjector).taskSupervisorSwitchUser(anyInt(), any()); doNothing().when(mInjector).taskSupervisorResumeFocusedStackTopActivity(); doNothing().when(mInjector).systemServiceManagerOnUserStopped(anyInt()); + doNothing().when(mInjector).systemServiceManagerOnUserCompletedEvent( + anyInt(), anyInt()); doNothing().when(mInjector).activityManagerForceStopPackage(anyInt(), anyString()); doNothing().when(mInjector).activityManagerOnUserStopped(anyInt()); doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt()); @@ -719,6 +724,54 @@ public class UserControllerTest { } } + @Test + public void testScheduleOnUserCompletedEvent() throws Exception { + // user1 is starting, switching, and unlocked, but not scheduled unlocked yet + // user2 is starting and had unlocked but isn't unlocked anymore for whatever reason + + final int user1 = 101; + final int user2 = 102; + setUpUser(user1, 0); + setUpUser(user2, 0); + + mUserController.startUser(user1, /* foreground= */ true); + mUserController.getStartedUserState(user1).setState(UserState.STATE_RUNNING_UNLOCKED); + + mUserController.startUser(user2, /* foreground= */ false); + mUserController.getStartedUserState(user2).setState(UserState.STATE_RUNNING_LOCKED); + + final int event1a = SystemService.UserCompletedEventType.EVENT_TYPE_USER_STARTING; + final int event1b = SystemService.UserCompletedEventType.EVENT_TYPE_USER_SWITCHING; + + final int event2a = SystemService.UserCompletedEventType.EVENT_TYPE_USER_STARTING; + final int event2b = SystemService.UserCompletedEventType.EVENT_TYPE_USER_UNLOCKED; + + + mUserController.scheduleOnUserCompletedEvent(user1, event1a, 2000); + assertNotNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user1)); + assertNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user2)); + + mUserController.scheduleOnUserCompletedEvent(user2, event2a, 2000); + assertNotNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user1)); + assertNotNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user2)); + + mUserController.scheduleOnUserCompletedEvent(user2, event2b, 2000); + mUserController.scheduleOnUserCompletedEvent(user1, event1b, 2000); + mUserController.scheduleOnUserCompletedEvent(user1, 0, 2000); + + assertNotNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user1)); + assertNotNull(mInjector.mHandler.getMessageForCode(USER_COMPLETED_EVENT_MSG, user2)); + + mUserController.reportOnUserCompletedEvent(user1); + verify(mInjector, times(1)) + .systemServiceManagerOnUserCompletedEvent(eq(user1), eq(event1a | event1b)); + verify(mInjector, never()).systemServiceManagerOnUserCompletedEvent(eq(user2), anyInt()); + + mUserController.reportOnUserCompletedEvent(user2); + verify(mInjector, times(1)) + .systemServiceManagerOnUserCompletedEvent(eq(user2), eq(event2a)); + } + private void setUpAndStartUserInBackground(int userId) throws Exception { setUpUser(userId, 0); mUserController.startUser(userId, /* foreground= */ false); @@ -969,8 +1022,12 @@ public class UserControllerTest { } Message getMessageForCode(int what) { + return getMessageForCode(what, null); + } + + Message getMessageForCode(int what, Object obj) { for (Message msg : mMessages) { - if (msg.what == what) { + if (msg.what == what && (obj == null || obj.equals(msg.obj))) { return msg; } }