SystemService post-completed user event

Introduces a new SystemService stage that indicates that user events
(user-switch, user-starting, user-unlocked) have completed. This can be
used by services for non-urgent tasks that must be completed after the
event in question, but can be delayed until later.

As long as they happen close enough in time, only a single event will be
sent (per user) which will contain information about all the
onUser-completion events it represents.

TODO: Also trigger the call when the user-switch UX has completed
(rather than waiting for the hard-coded delay). First we need to find an
appropriate signal for "switch UX is complete".

Test: atest UserControllerTest
Test: manual confirmation using VoiceInteractionManagerService
Bug: 197344658
Change-Id: Id8e6ce8e9c46f86d48d0a76085123c0c3dd1f2ea
This commit is contained in:
Adam Bookatz
2022-01-12 09:56:18 -08:00
parent 12091ce823
commit 69444e10e8
6 changed files with 355 additions and 16 deletions

View File

@@ -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 <var>object</var> is null,
* all messages will be removed.
* <p>
* 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
*/

View File

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

View File

@@ -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 <i>after</i> 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;

View File

@@ -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)

View File

@@ -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<UserJourneySession> 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() {

View File

@@ -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;
}
}