diff options
7 files changed, 1289 insertions, 438 deletions
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 24e28e95cd98..5bcbaa10e95b 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -65,7 +65,6 @@ import android.util.Log; import android.view.WindowManager.LayoutParams; import com.android.internal.R; -import com.android.internal.util.FrameworkStatsLog; import java.io.IOException; import java.lang.annotation.Retention; @@ -2533,38 +2532,6 @@ public class UserManager { } /** - * Returns the enum defined in the statsd UserLifecycleJourneyReported atom corresponding to the - * user type. - * @hide - */ - public static int getUserTypeForStatsd(@NonNull String userType) { - switch (userType) { - case USER_TYPE_FULL_SYSTEM: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SYSTEM; - case USER_TYPE_FULL_SECONDARY: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY; - case USER_TYPE_FULL_GUEST: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_GUEST; - case USER_TYPE_FULL_DEMO: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_DEMO; - case USER_TYPE_FULL_RESTRICTED: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_RESTRICTED; - case USER_TYPE_PROFILE_MANAGED: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_MANAGED; - case USER_TYPE_SYSTEM_HEADLESS: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__SYSTEM_HEADLESS; - case USER_TYPE_PROFILE_CLONE: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_CLONE; - default: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN; - } - } - - /** * @hide * @deprecated Use {@link #isRestrictedProfile()} */ diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 5a4d315767ca..4a69f90d9fc0 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -67,7 +67,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; -import android.os.UserManager; import android.text.TextUtils; import android.util.EventLog; import android.util.IndentingPrintWriter; @@ -78,6 +77,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.os.TimeoutRecord; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; +import com.android.server.pm.UserJourneyLogger; import com.android.server.pm.UserManagerInternal; import dalvik.annotation.optimization.NeverCompile; @@ -1518,7 +1518,7 @@ public class BroadcastQueueImpl extends BroadcastQueue { final UserInfo userInfo = (umInternal != null) ? umInternal.getUserInfo(r.userId) : null; if (userInfo != null) { - userType = UserManager.getUserTypeForStatsd(userInfo.userType); + userType = UserJourneyLogger.getUserTypeForStatsd(userInfo.userType); } Slog.i(TAG_BROADCAST, "BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED action:" diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index b2fdee7a6f89..06af2ce9d655 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -46,13 +46,24 @@ import static com.android.server.am.UserState.STATE_BOOTING; import static com.android.server.am.UserState.STATE_RUNNING_LOCKED; import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED; import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKING; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_INVALID_SESSION_ID; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_BEGIN; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_UNLOCKED_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_UNLOCKING_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE; import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND; import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; import static com.android.server.pm.UserManagerInternal.userStartModeToString; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -123,6 +134,8 @@ 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.UserJourneyLogger; +import com.android.server.pm.UserJourneyLogger.UserJourneySession; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserLifecycleListener; import com.android.server.pm.UserManagerInternal.UserStartMode; @@ -138,7 +151,6 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -221,75 +233,6 @@ class UserController implements Handler.Callback { // 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; - - // The various user journeys, defined in the UserLifecycleJourneyReported atom for statsd - private static final int USER_JOURNEY_UNKNOWN = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__UNKNOWN; - private static final int USER_JOURNEY_USER_SWITCH_FG = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_FG; - private static final int USER_JOURNEY_USER_SWITCH_UI = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_UI; - private static final int USER_JOURNEY_USER_START = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_START; - private static final int USER_JOURNEY_USER_CREATE = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE; - private static final int USER_JOURNEY_USER_STOP = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_STOP; - @IntDef(prefix = { "USER_JOURNEY" }, value = { - USER_JOURNEY_UNKNOWN, - USER_JOURNEY_USER_SWITCH_FG, - USER_JOURNEY_USER_SWITCH_UI, - USER_JOURNEY_USER_START, - USER_JOURNEY_USER_CREATE, - USER_JOURNEY_USER_STOP - }) - @interface UserJourney {} - - // The various user lifecycle events, defined in the UserLifecycleEventOccurred atom for statsd - private static final int USER_LIFECYCLE_EVENT_UNKNOWN = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; - private static final int USER_LIFECYCLE_EVENT_SWITCH_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__SWITCH_USER; - private static final int USER_LIFECYCLE_EVENT_START_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__START_USER; - private static final int USER_LIFECYCLE_EVENT_CREATE_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - private static final int USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__USER_RUNNING_LOCKED; - private static final int USER_LIFECYCLE_EVENT_UNLOCKING_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKING_USER; - private static final int USER_LIFECYCLE_EVENT_UNLOCKED_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKED_USER; - private static final int USER_LIFECYCLE_EVENT_STOP_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__STOP_USER; - @IntDef(prefix = { "USER_LIFECYCLE_EVENT" }, value = { - USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_START_USER, - USER_LIFECYCLE_EVENT_CREATE_USER, - USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, - USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_UNLOCKED_USER, - USER_LIFECYCLE_EVENT_STOP_USER - }) - @interface UserLifecycleEvent {} - - // User lifecyle event state, defined in the UserLifecycleEventOccurred atom for statsd - private static final int USER_LIFECYCLE_EVENT_STATE_BEGIN = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN; - private static final int USER_LIFECYCLE_EVENT_STATE_FINISH = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH; - private static final int USER_LIFECYCLE_EVENT_STATE_NONE = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE; - @IntDef(prefix = { "USER_LIFECYCLE_EVENT_STATE" }, value = { - USER_LIFECYCLE_EVENT_STATE_BEGIN, - USER_LIFECYCLE_EVENT_STATE_FINISH, - USER_LIFECYCLE_EVENT_STATE_NONE, - }) - @interface UserLifecycleEventState {} - /** * Maximum number of users we allow to be running at a time, including system user. * @@ -420,13 +363,6 @@ class UserController implements Handler.Callback { private final ArrayList<Integer> mLastActiveUsers = new ArrayList<>(); /** - * {@link UserIdInt} to {@link UserJourneySession} mapping used for statsd logging for the - * UserLifecycleJourneyReported and UserLifecycleEventOccurred atoms. - */ - @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. */ @@ -621,8 +557,9 @@ class UserController implements Handler.Callback { // but we might immediately step into RUNNING below if the user // storage is already unlocked. if (uss.setState(STATE_BOOTING, STATE_RUNNING_LOCKED)) { - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, - USER_LIFECYCLE_EVENT_STATE_NONE); + mInjector.getUserJourneyLogger() + .logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE); mInjector.getUserManagerInternal().setUserState(userId, uss.state); // Do not report secondary users, runtime restarts or first boot/upgrade if (userId == UserHandle.USER_SYSTEM @@ -694,8 +631,9 @@ class UserController implements Handler.Callback { private boolean finishUserUnlocking(final UserState uss) { final int userId = uss.mHandle.getIdentifier(); EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId); - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + mInjector.getUserJourneyLogger() + .logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER, + EVENT_STATE_BEGIN); // If the user key hasn't been unlocked yet, we cannot proceed. if (!StorageManager.isUserKeyUnlocked(userId)) return false; synchronized (mLock) { @@ -1073,9 +1011,7 @@ class UserController implements Handler.Callback { return; } - logUserJourneyInfo(null, getUserInfo(userId), USER_JOURNEY_USER_STOP); - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(userId, USER_JOURNEY_USER_STOP); if (stopUserCallback != null) { uss.mStopCallbacks.add(stopUserCallback); @@ -1138,9 +1074,16 @@ class UserController implements Handler.Callback { synchronized (mLock) { if (uss.state != UserState.STATE_STOPPING) { // Whoops, we are being started back up. Abort, abort! - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_NONE); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserJourneyFinishWithError(-1, getUserInfo(userId), + USER_JOURNEY_USER_STOP, ERROR_CODE_ABORTED); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } else { + mInjector.getUserJourneyLogger() + .logUserJourneyFinishWithError(-1, getUserInfo(userId), + USER_JOURNEY_USER_STOP, ERROR_CODE_INVALID_SESSION_ID); + } return; } uss.setState(UserState.STATE_SHUTDOWN); @@ -1247,9 +1190,11 @@ class UserController implements Handler.Callback { mInjector.getUserManager().removeUserEvenWhenDisallowed(userId); } - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserJourneyFinish(-1, userInfo, USER_JOURNEY_USER_STOP); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } if (lockUser) { dispatchUserLocking(userIdToLock, keyEvictedCallbacks); @@ -1259,9 +1204,11 @@ class UserController implements Handler.Callback { // which was paused while the SHUTDOWN flow of the user was in progress. resumePendingUserStarts(userId); } else { - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_NONE); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(userId, USER_JOURNEY_USER_STOP); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } } } @@ -3135,10 +3082,7 @@ class UserController implements Handler.Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case START_USER_SWITCH_FG_MSG: - logUserJourneyInfo(getUserInfo(getCurrentUserId()), getUserInfo(msg.arg1), - USER_JOURNEY_USER_SWITCH_FG); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(msg.arg1, USER_JOURNEY_USER_SWITCH_FG); startUserInForeground(msg.arg1); break; case REPORT_USER_SWITCH_MSG: @@ -3160,18 +3104,15 @@ class UserController implements Handler.Callback { mInjector.batteryStatsServiceNoteEvent( BatteryStats.HistoryItem.EVENT_USER_RUNNING_START, Integer.toString(msg.arg1), msg.arg1); - logUserJourneyInfo(null, getUserInfo(msg.arg1), USER_JOURNEY_USER_START); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(msg.arg1, USER_JOURNEY_USER_START); mInjector.onUserStarting(/* userId= */ 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); - clearSessionId(msg.arg1, USER_JOURNEY_USER_START); + mInjector.getUserJourneyLogger().logUserJourneyFinish(-1 , getUserInfo(msg.arg1), + USER_JOURNEY_USER_START); break; case USER_UNLOCK_MSG: final int userId = msg.arg1; @@ -3180,10 +3121,11 @@ class UserController implements Handler.Callback { FgThread.getHandler().post(() -> { mInjector.loadUserRecents(userId); }); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKED_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKING_USER, EVENT_STATE_FINISH); + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, EVENT_STATE_BEGIN); final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("finishUserUnlocked-" + userId); @@ -3199,9 +3141,9 @@ class UserController implements Handler.Callback { // (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); + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, EVENT_STATE_FINISH); + // Unlocking user is not a journey no need to clear sessionId break; case USER_CURRENT_MSG: mInjector.batteryStatsServiceNoteEvent( @@ -3224,22 +3166,24 @@ class UserController implements Handler.Callback { break; case REPORT_USER_SWITCH_COMPLETE_MSG: dispatchUserSwitchComplete(msg.arg1, msg.arg2); - logUserLifecycleEvent(msg.arg2, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserSwitchJourneyFinish(msg.arg1, getUserInfo(msg.arg2)); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } break; case REPORT_LOCKED_BOOT_COMPLETE_MSG: dispatchLockedBootComplete(msg.arg1); break; case START_USER_SWITCH_UI_MSG: final Pair<UserInfo, UserInfo> fromToUserPair = (Pair<UserInfo, UserInfo>) msg.obj; - logUserJourneyInfo(fromToUserPair.first, fromToUserPair.second, - USER_JOURNEY_USER_SWITCH_UI); - logUserLifecycleEvent(fromToUserPair.second.id, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(fromToUserPair.second.id, USER_JOURNEY_USER_SWITCH_UI); showUserSwitchDialog(fromToUserPair); break; case CLEAR_USER_JOURNEY_SESSION_MSG: - logAndClearSessionId(msg.arg1); + mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(msg.arg1, msg.arg2); + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, msg.obj); break; case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1, msg.arg2); @@ -3317,123 +3261,29 @@ class UserController implements Handler.Callback { * statsd helper method for logging the start of a user journey via a UserLifecycleEventOccurred * atom given the originating and targeting users for the journey. */ - private void logUserJourneyInfo(UserInfo origin, UserInfo target, @UserJourney int journey) { - final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); - synchronized (mUserIdToUserJourneyMap) { - UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(target.id); - if (userJourneySession != null) { - // TODO(b/157007231): Move this logic to a separate class/file. - if ((userJourneySession.mJourney == USER_JOURNEY_USER_SWITCH_UI - || userJourneySession.mJourney == USER_JOURNEY_USER_SWITCH_FG) - && (journey == USER_JOURNEY_USER_START - || journey == USER_JOURNEY_USER_STOP)) { - /* - * There is already a user switch journey, and a user start or stop journey for - * the same target user received. New journey is most likely a part of user - * switch journey so no need to create a new journey. - */ - if (DEBUG_MU) { - Slogf.d(TAG, journey + " not logged as it is expected to be part of " - + userJourneySession.mJourney); - } - return; - } - /* - * Possible reasons for this condition to be true: - * - A user switch journey is received while another user switch journey is in - * process for the same user. - * - A user switch journey is received while user start journey is in process for - * the same user. - * - A user start journey is received while another user start journey is in process - * for the same user. - * In all cases potentially an incomplete, timed-out session or multiple - * simultaneous requests. It is not possible to keep track of multiple sessions for - * the same user, so previous session is abandoned. - */ - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, - userJourneySession.mSessionId, target.id, USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_STATE_NONE); - } - + private void logUserJourneyBegin(int targetId, + @UserJourneyLogger.UserJourney int journey) { + UserJourneySession oldSession = mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(targetId, journey); + if (oldSession != null) { if (DEBUG_MU) { Slogf.d(TAG, - "Starting a new journey: " + journey + " with session id: " + newSessionId); + "Starting a new journey: " + journey + " with session id: " + + oldSession); } - - userJourneySession = new UserJourneySession(newSessionId, journey); - mUserIdToUserJourneyMap.put(target.id, userJourneySession); /* - * User lifecyle journey would be complete when {@code #clearSessionId} is called after - * the last expected lifecycle event for the journey. It may be possible that the last - * event is not called, e.g., user not unlocked after user switching. In such cases user - * journey is cleared after {@link USER_JOURNEY_TIMEOUT}. + * User lifecycle journey would be complete when {@code #clearSessionId} is called + * after the last expected lifecycle event for the journey. It may be possible that + * the last event is not called, e.g., user not unlocked after user switching. In such + * cases user journey is cleared after {@link USER_JOURNEY_TIMEOUT}. */ - mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG); - mHandler.sendMessageDelayed(mHandler.obtainMessage(CLEAR_USER_JOURNEY_SESSION_MSG, - target.id, /* arg2= */ 0), USER_JOURNEY_TIMEOUT_MS); - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, newSessionId, - journey, origin != null ? origin.id : -1, - target.id, UserManager.getUserTypeForStatsd(target.userType), target.flags); - } - - /** - * statsd helper method for logging the given event for the UserLifecycleEventOccurred statsd - * atom. - */ - private void logUserLifecycleEvent(@UserIdInt int userId, @UserLifecycleEvent int event, - @UserLifecycleEventState int eventState) { - final long sessionId; - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession == null || userJourneySession.mSessionId == INVALID_SESSION_ID) { - Slogf.w(TAG, "UserLifecycleEvent " + event - + " received without an active userJourneySession."); - return; - } - sessionId = userJourneySession.mSessionId; - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, eventState); - } - - /** - * Clears the {@link UserJourneySession} for a given {@link UserIdInt} and {@link UserJourney}. - */ - private void clearSessionId(@UserIdInt int userId, @UserJourney int journey) { - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession != null && userJourneySession.mJourney == journey) { - clearSessionId(userId); - } + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, oldSession); } - } + UserJourneySession newSession = mInjector.getUserJourneyLogger() + .logUserJourneyBegin(targetId, journey); + mHandler.sendMessageDelayed(mHandler.obtainMessage(CLEAR_USER_JOURNEY_SESSION_MSG, + targetId, /* arg2= */ journey, newSession), USER_JOURNEY_TIMEOUT_MS); - /** - * Clears the {@link UserJourneySession} for a given {@link UserIdInt}. - */ - private void clearSessionId(@UserIdInt int userId) { - synchronized (mUserIdToUserJourneyMap) { - mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG); - mUserIdToUserJourneyMap.delete(userId); - } - } - - /** - * Log a final event of the {@link UserJourneySession} and clear it. - */ - private void logAndClearSessionId(@UserIdInt int userId) { - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession != null) { - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, - userJourneySession.mSessionId, userId, USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_STATE_NONE); - } - clearSessionId(userId); - } } private BroadcastOptions getTemporaryAppAllowlistBroadcastOptions( @@ -3471,23 +3321,6 @@ class UserController implements Handler.Callback { return mLastUserUnlockingUptime; } - /** - * Helper class to store user journey and session id. - * - * <p> User journey tracks a chain of user lifecycle events occurring during different user - * activities such as user start, user switch, and user creation. - */ - // TODO(b/157007231): Move this class and user journey tracking logic to a separate file. - private static class UserJourneySession { - final long mSessionId; - @UserJourney final int mJourney; - - UserJourneySession(long sessionId, @UserJourney int journey) { - mJourney = journey; - mSessionId = sessionId; - } - } - private static class UserProgressListener extends IProgressListener.Stub { private volatile long mUnlockStarted; @Override @@ -3562,6 +3395,10 @@ class UserController implements Handler.Callback { return new Handler(mService.mUiHandler.getLooper(), callback); } + protected UserJourneyLogger getUserJourneyLogger() { + return getUserManager().getUserJourneyLogger(); + } + protected Context getContext() { return mService.mContext; } diff --git a/services/core/java/com/android/server/pm/UserJourneyLogger.java b/services/core/java/com/android/server/pm/UserJourneyLogger.java new file mode 100644 index 000000000000..f48a1669c259 --- /dev/null +++ b/services/core/java/com/android/server/pm/UserJourneyLogger.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.os.UserManager.USER_TYPE_FULL_DEMO; +import static android.os.UserManager.USER_TYPE_FULL_GUEST; +import static android.os.UserManager.USER_TYPE_FULL_RESTRICTED; +import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; +import static android.os.UserManager.USER_TYPE_FULL_SYSTEM; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; +import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; +import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; + +import static com.android.internal.util.FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.pm.UserInfo; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * This class is logging User Lifecycle statsd events and synchronise User Lifecycle Journeys + * by making sure all events are called in correct order and errors are reported in case of + * unexpected journeys. This class also makes sure that all user sub-journeys are logged so + * for example User Switch also log User Start Journey. + */ +public class UserJourneyLogger { + + public static final int ERROR_CODE_INVALID_SESSION_ID = 0; + public static final int ERROR_CODE_UNSPECIFIED = -1; + /* + * Possible reasons for ERROR_CODE_INCOMPLETE_OR_TIMEOUT to occur: + * - A user switch journey is received while another user switch journey is in + * process for the same user. + * - A user switch journey is received while user start journey is in process for + * the same user. + * - A user start journey is received while another user start journey is in process + * for the same user. + * In all cases potentially an incomplete, timed-out session or multiple + * simultaneous requests. It is not possible to keep track of multiple sessions for + * the same user, so previous session is abandoned. + */ + public static final int ERROR_CODE_INCOMPLETE_OR_TIMEOUT = 2; + public static final int ERROR_CODE_ABORTED = 3; + public static final int ERROR_CODE_NULL_USER_INFO = 4; + public static final int ERROR_CODE_USER_ALREADY_AN_ADMIN = 5; + public static final int ERROR_CODE_USER_IS_NOT_AN_ADMIN = 6; + + @IntDef(prefix = {"ERROR_CODE"}, value = { + ERROR_CODE_UNSPECIFIED, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, + ERROR_CODE_ABORTED, + ERROR_CODE_NULL_USER_INFO, + ERROR_CODE_USER_ALREADY_AN_ADMIN, + ERROR_CODE_USER_IS_NOT_AN_ADMIN, + ERROR_CODE_INVALID_SESSION_ID + }) + public @interface UserJourneyErrorCode { + } + + // The various user journeys, defined in the UserLifecycleJourneyReported atom for statsd + public static final int USER_JOURNEY_UNKNOWN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__UNKNOWN; + public static final int USER_JOURNEY_USER_SWITCH_FG = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_FG; + public static final int USER_JOURNEY_USER_SWITCH_UI = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_UI; + public static final int USER_JOURNEY_USER_START = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_START; + public static final int USER_JOURNEY_USER_CREATE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE; + public static final int USER_JOURNEY_USER_STOP = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_STOP; + public static final int USER_JOURNEY_USER_REMOVE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE; + public static final int USER_JOURNEY_GRANT_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN; + public static final int USER_JOURNEY_REVOKE_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN; + + @IntDef(prefix = {"USER_JOURNEY"}, value = { + USER_JOURNEY_UNKNOWN, + USER_JOURNEY_USER_SWITCH_FG, + USER_JOURNEY_USER_SWITCH_UI, + USER_JOURNEY_USER_START, + USER_JOURNEY_USER_STOP, + USER_JOURNEY_USER_CREATE, + USER_JOURNEY_USER_REMOVE, + USER_JOURNEY_GRANT_ADMIN, + USER_JOURNEY_REVOKE_ADMIN + }) + public @interface UserJourney { + } + + + // The various user lifecycle events, defined in the UserLifecycleEventOccurred atom for statsd + public static final int USER_LIFECYCLE_EVENT_UNKNOWN = + USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; + public static final int USER_LIFECYCLE_EVENT_SWITCH_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__SWITCH_USER; + public static final int USER_LIFECYCLE_EVENT_START_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__START_USER; + public static final int USER_LIFECYCLE_EVENT_CREATE_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; + public static final int USER_LIFECYCLE_EVENT_REMOVE_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; + public static final int USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__USER_RUNNING_LOCKED; + public static final int USER_LIFECYCLE_EVENT_UNLOCKING_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKING_USER; + public static final int USER_LIFECYCLE_EVENT_UNLOCKED_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKED_USER; + public static final int USER_LIFECYCLE_EVENT_STOP_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__STOP_USER; + public static final int USER_LIFECYCLE_EVENT_GRANT_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; + public static final int USER_LIFECYCLE_EVENT_REVOKE_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; + + @IntDef(prefix = {"USER_LIFECYCLE_EVENT"}, value = { + USER_LIFECYCLE_EVENT_UNKNOWN, + USER_LIFECYCLE_EVENT_SWITCH_USER, + USER_LIFECYCLE_EVENT_START_USER, + USER_LIFECYCLE_EVENT_CREATE_USER, + USER_LIFECYCLE_EVENT_REMOVE_USER, + USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + USER_LIFECYCLE_EVENT_UNLOCKING_USER, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, + USER_LIFECYCLE_EVENT_STOP_USER, + USER_LIFECYCLE_EVENT_GRANT_ADMIN, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN + }) + public @interface UserLifecycleEvent { + } + + // User lifecycle event state, defined in the UserLifecycleEventOccurred atom for statsd + public static final int EVENT_STATE_BEGIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN; + public static final int EVENT_STATE_FINISH = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH; + public static final int EVENT_STATE_NONE = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE; + public static final int EVENT_STATE_CANCEL = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__CANCEL; + public static final int EVENT_STATE_ERROR = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__ERROR; + + @IntDef(prefix = {"EVENT_STATE"}, value = { + EVENT_STATE_BEGIN, + EVENT_STATE_FINISH, + EVENT_STATE_NONE, + EVENT_STATE_CANCEL, + EVENT_STATE_ERROR, + }) + public @interface UserLifecycleEventState { + } + + private static final int USER_ID_KEY_MULTIPLICATION = 100; + + private final Object mLock = new Object(); + + /** + * {@link UserIdInt} and {@link UserJourney} to {@link UserJourneySession} mapping used for + * statsd logging for the UserLifecycleJourneyReported and UserLifecycleEventOccurred atoms. + */ + @GuardedBy("mLock") + private final SparseArray<UserJourneySession> mUserIdToUserJourneyMap = new SparseArray<>(); + + /** + * Returns event equivalent of given journey + */ + @UserLifecycleEvent + private static int journeyToEvent(@UserJourney int journey) { + switch (journey) { + case USER_JOURNEY_USER_SWITCH_UI: + case USER_JOURNEY_USER_SWITCH_FG: + return USER_LIFECYCLE_EVENT_SWITCH_USER; + case USER_JOURNEY_USER_START: + return USER_LIFECYCLE_EVENT_START_USER; + case USER_JOURNEY_USER_CREATE: + return USER_LIFECYCLE_EVENT_CREATE_USER; + case USER_JOURNEY_USER_STOP: + return USER_LIFECYCLE_EVENT_STOP_USER; + case USER_JOURNEY_USER_REMOVE: + return USER_LIFECYCLE_EVENT_REMOVE_USER; + case USER_JOURNEY_GRANT_ADMIN: + return USER_LIFECYCLE_EVENT_GRANT_ADMIN; + case USER_JOURNEY_REVOKE_ADMIN: + return USER_LIFECYCLE_EVENT_REVOKE_ADMIN; + default: + return USER_LIFECYCLE_EVENT_UNKNOWN; + } + } + + /** + * Returns the enum defined in the statsd UserLifecycleJourneyReported atom corresponding to + * the user type. + * Changes to this method require changes in CTS file + * com.android.cts.packagemanager.stats.device.UserInfoUtil + * which is duplicate for CTS tests purposes. + */ + public static int getUserTypeForStatsd(@NonNull String userType) { + switch (userType) { + case USER_TYPE_FULL_SYSTEM: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SYSTEM; + case USER_TYPE_FULL_SECONDARY: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY; + case USER_TYPE_FULL_GUEST: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_GUEST; + case USER_TYPE_FULL_DEMO: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_DEMO; + case USER_TYPE_FULL_RESTRICTED: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_RESTRICTED; + case USER_TYPE_PROFILE_MANAGED: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_MANAGED; + case USER_TYPE_SYSTEM_HEADLESS: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__SYSTEM_HEADLESS; + case USER_TYPE_PROFILE_CLONE: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_CLONE; + default: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN; + } + } + + /** + * Map error code to the event finish state. + */ + @UserLifecycleEventState + private static int errorToFinishState(@UserJourneyErrorCode int errorCode) { + switch (errorCode) { + case ERROR_CODE_ABORTED: + return EVENT_STATE_CANCEL; + case ERROR_CODE_UNSPECIFIED: + return EVENT_STATE_FINISH; + default: + return EVENT_STATE_ERROR; + } + } + + /** + * Simply logging USER_LIFECYCLE_JOURNEY_REPORTED if session exists. + * If session does not exist then it logs ERROR_CODE_INVALID_SESSION_ID + */ + @VisibleForTesting + public void logUserLifecycleJourneyReported(@Nullable UserJourneySession session, + @UserJourney int journey, @UserIdInt int originalUserId, @UserIdInt int targetUserId, + int userType, int userFlags, @UserJourneyErrorCode int errorCode) { + if (session == null) { + writeUserLifecycleJourneyReported(-1, journey, originalUserId, targetUserId, + userType, userFlags, ERROR_CODE_INVALID_SESSION_ID); + } else { + writeUserLifecycleJourneyReported( + session.mSessionId, journey, originalUserId, targetUserId, userType, userFlags, + errorCode); + } + } + + /** + * Helper method for spy testing + */ + @VisibleForTesting + public void writeUserLifecycleJourneyReported(long sessionId, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode) { + FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, + sessionId, journey, originalUserId, targetUserId, userType, userFlags, + errorCode); + } + + /** + * Simply logging USER_LIFECYCLE_EVENT_OCCURRED if session exists. + * If session does not exist then it logs ERROR_CODE_INVALID_SESSION_ID + * and EVENT_STATE_ERROR + */ + @VisibleForTesting + public void logUserLifecycleEventOccurred(UserJourneySession session, + @UserIdInt int targetUserId, @UserLifecycleEvent int event, + @UserLifecycleEventState int state, @UserJourneyErrorCode int errorCode) { + if (session == null) { + writeUserLifecycleEventOccurred(-1, targetUserId, event, + EVENT_STATE_ERROR, ERROR_CODE_INVALID_SESSION_ID); + } else { + writeUserLifecycleEventOccurred(session.mSessionId, targetUserId, event, state, + errorCode); + } + } + + /** + * Helper method for spy testing + */ + @VisibleForTesting + public void writeUserLifecycleEventOccurred(long sessionId, int userId, int event, int state, + int errorCode) { + FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, + sessionId, userId, event, state, errorCode); + } + + /** + * statsd helper method for logging the given event for the UserLifecycleEventOccurred statsd + * atom. It finds the user journey session for target user id and logs it as that journey. + */ + public void logUserLifecycleEvent(@UserIdInt int userId, @UserLifecycleEvent int event, + @UserLifecycleEventState int eventState) { + final UserJourneySession userJourneySession = findUserJourneySession(userId); + logUserLifecycleEventOccurred(userJourneySession, userId, + event, eventState, UserJourneyLogger.ERROR_CODE_UNSPECIFIED); + } + + /** + * Returns first user session from mUserIdToUserJourneyMap for given user id, + * or null if user id was not found in mUserIdToUserJourneyMap. + */ + private @Nullable UserJourneySession findUserJourneySession(@UserIdInt int userId) { + synchronized (mLock) { + final int keyMapSize = mUserIdToUserJourneyMap.size(); + for (int i = 0; i < keyMapSize; i++) { + int key = mUserIdToUserJourneyMap.keyAt(i); + if (key / USER_ID_KEY_MULTIPLICATION == userId) { + return mUserIdToUserJourneyMap.get(key); + } + } + } + return null; + } + + /** + * Returns unique id for user and journey. For example if user id = 11 and journey = 7 + * then unique key = 11 * 100 + 7 = 1107 + */ + private int getUserJourneyKey(@UserIdInt int targetUserId, @UserJourney int journey) { + // We leave 99 for user journeys ids. + return (targetUserId * USER_ID_KEY_MULTIPLICATION) + journey; + } + + /** + * Special use case when user journey incomplete or timeout and current user is unclear + */ + @VisibleForTesting + public UserJourneySession finishAndClearIncompleteUserJourney(@UserIdInt int targetUserId, + @UserJourney int journey) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUserId, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, + targetUserId, + journeyToEvent(userJourneySession.mJourney), + EVENT_STATE_ERROR, + UserJourneyLogger.ERROR_CODE_INCOMPLETE_OR_TIMEOUT); + + logUserLifecycleJourneyReported( + userJourneySession, + journey, + /* originalUserId= */ -1, + targetUserId, + getUserTypeForStatsd(""), -1, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Log user journey event and report finishing without error + */ + public UserJourneySession logUserJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey) { + return logUserJourneyFinishWithError(originalUserId, targetUser, journey, + ERROR_CODE_UNSPECIFIED); + } + + /** + * Special case when it is unknown which user switch journey was used and checking both + */ + @VisibleForTesting + public UserJourneySession logUserSwitchJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser) { + synchronized (mLock) { + final int key_fg = getUserJourneyKey(targetUser.id, USER_JOURNEY_USER_SWITCH_FG); + final int key_ui = getUserJourneyKey(targetUser.id, USER_JOURNEY_USER_SWITCH_UI); + + if (mUserIdToUserJourneyMap.contains(key_fg)) { + return logUserJourneyFinish(originalUserId, targetUser, + USER_JOURNEY_USER_SWITCH_FG); + } + + if (mUserIdToUserJourneyMap.contains(key_ui)) { + return logUserJourneyFinish(originalUserId, targetUser, + USER_JOURNEY_USER_SWITCH_UI); + } + + return null; + } + } + + /** + * Log user journey event and report finishing with error + */ + public UserJourneySession logUserJourneyFinishWithError(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey, @UserJourneyErrorCode int errorCode) { + synchronized (mLock) { + final int state = errorToFinishState(errorCode); + final int key = getUserJourneyKey(targetUser.id, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, targetUser.id, + journeyToEvent(userJourneySession.mJourney), + state, + errorCode); + + logUserLifecycleJourneyReported( + userJourneySession, + journey, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + errorCode); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Log event and report finish when user is null. This is edge case when UserInfo + * can not be passed because it is null, therefore all information are passed as arguments. + */ + public UserJourneySession logNullUserJourneyError(@UserJourney int journey, + @UserIdInt int currentUserId, @UserIdInt int targetUserId, String targetUserType, + int targetUserFlags) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUserId, journey); + final UserJourneySession session = mUserIdToUserJourneyMap.get(key); + + logUserLifecycleEventOccurred( + session, targetUserId, journeyToEvent(journey), + EVENT_STATE_ERROR, + ERROR_CODE_NULL_USER_INFO); + + logUserLifecycleJourneyReported( + session, journey, currentUserId, targetUserId, + getUserTypeForStatsd(targetUserType), targetUserFlags, + ERROR_CODE_NULL_USER_INFO); + + mUserIdToUserJourneyMap.remove(key); + return session; + } + } + + /** + * Log for user creation finish event and report. This is edge case when target user id is + * different in begin event and finish event as it is unknown what is user id + * until it has been created. + */ + public UserJourneySession logUserCreateJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser) { + synchronized (mLock) { + // we do not know user id until we create new user which is why we use -1 + // as user id to create and find session, but we log correct id. + final int key = getUserJourneyKey(-1, USER_JOURNEY_USER_CREATE); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, targetUser.id, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_FINISH, + ERROR_CODE_UNSPECIFIED); + + logUserLifecycleJourneyReported( + userJourneySession, + USER_JOURNEY_USER_CREATE, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + ERROR_CODE_UNSPECIFIED); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Adds new UserJourneySession to mUserIdToUserJourneyMap and log UserJourneyEvent Begin state + */ + public UserJourneySession logUserJourneyBegin(@UserIdInt int targetId, + @UserJourney int journey) { + final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); + synchronized (mLock) { + final int key = getUserJourneyKey(targetId, journey); + final UserJourneySession userJourneySession = + new UserJourneySession(newSessionId, journey); + mUserIdToUserJourneyMap.append(key, userJourneySession); + + logUserLifecycleEventOccurred( + userJourneySession, targetId, + journeyToEvent(userJourneySession.mJourney), + EVENT_STATE_BEGIN, + ERROR_CODE_UNSPECIFIED); + + return userJourneySession; + } + } + + /** + * Helper class to store user journey and session id. + * + * <p> User journey tracks a chain of user lifecycle events occurring during different user + * activities such as user start, user switch, and user creation. + */ + public static class UserJourneySession { + public final long mSessionId; + @UserJourney + public final int mJourney; + + @VisibleForTesting + public UserJourneySession(long sessionId, @UserJourney int journey) { + mJourney = journey; + mSessionId = sessionId; + } + } +} diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 5f8efe29459d..b92cdde5910f 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -23,6 +23,15 @@ import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_UNSPECIFIED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_ALREADY_AN_ADMIN; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_NOT_AN_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; + import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; @@ -162,7 +171,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -519,6 +527,8 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserLifecycleListeners") private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>(); + private final UserJourneyLogger mUserJourneyLogger = new UserJourneyLogger(); + private final LockPatternUtils mLockPatternUtils; private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = @@ -1580,45 +1590,56 @@ public class UserManagerService extends IUserManager.Stub { @Override public void setUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("set user admin"); - final long sessionId = logGrantAdminJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN); UserInfo info; synchronized (mPackagesLock) { synchronized (mUsersLock) { info = getUserInfoLU(userId); } - if (info == null || info.isAdmin()) { - // Exit if no user found with that id, or the user is already an Admin. - logUserJourneyError(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId); + if (info == null) { + // Exit if no user found with that id, + mUserJourneyLogger.logNullUserJourneyError(USER_JOURNEY_GRANT_ADMIN, + getCurrentUserId(), userId, /* userType */ "", /* userFlags */ -1); + return; + } else if (info.isAdmin()) { + // Exit if the user is already an Admin. + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), info, + USER_JOURNEY_GRANT_ADMIN, ERROR_CODE_USER_ALREADY_AN_ADMIN); return; } info.flags ^= UserInfo.FLAG_ADMIN; writeUserLP(getUserDataLU(info.id)); } - logGrantAdminJourneyFinish(sessionId, userId, info.userType, info.flags); + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), info, + USER_JOURNEY_GRANT_ADMIN, ERROR_CODE_UNSPECIFIED); } @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); - final long sessionId = logRevokeAdminJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN); UserData user; synchronized (mPackagesLock) { synchronized (mUsersLock) { user = getUserDataLU(userId); - if (user == null || !user.info.isAdmin()) { - // Exit if no user found with that id, or the user is not an Admin. - logUserJourneyError(sessionId, FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId); + if (user == null) { + // Exit if no user found with that id + mUserJourneyLogger.logNullUserJourneyError( + USER_JOURNEY_REVOKE_ADMIN, + getCurrentUserId(), userId, "", -1); + return; + } else if (!user.info.isAdmin()) { + // Exit if no user is not an Admin. + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), user.info, + USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_USER_IS_NOT_AN_ADMIN); return; } user.info.flags ^= UserInfo.FLAG_ADMIN; writeUserLP(user); } } - logRevokeAdminJourneyFinish(sessionId, userId, user.info.userType, user.info.flags); + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), user.info, + USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_UNSPECIFIED); } /** @@ -4700,16 +4721,20 @@ public class UserManagerService extends IUserManager.Stub { final int noneUserId = -1; final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("createUser-" + flags); - final long sessionId = logUserCreateJourneyBegin(noneUserId); + mUserJourneyLogger.logUserJourneyBegin(noneUserId, USER_JOURNEY_USER_CREATE); UserInfo newUser = null; try { newUser = createUserInternalUncheckedNoTracing(name, userType, flags, parentId, preCreate, disallowedPackages, t, token); return newUser; } finally { - logUserCreateJourneyFinish(sessionId, - newUser != null ? newUser.id : noneUserId, userType, flags, - newUser != null); + if (newUser != null) { + mUserJourneyLogger.logUserCreateJourneyFinish(getCurrentUserId(), newUser); + } else { + mUserJourneyLogger.logNullUserJourneyError( + USER_JOURNEY_USER_CREATE, + getCurrentUserId(), noneUserId, userType, flags); + } t.traceEnd(); } } @@ -5198,137 +5223,6 @@ public class UserManagerService extends IUserManager.Stub { && !userTypeDetails.getName().equals(UserManager.USER_TYPE_FULL_RESTRICTED); } - private long logUserCreateJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE, - userId); - } - - private void logUserCreateJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags, boolean finish) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE, - userId, userType, flags, finish); - } - - private long logUserRemoveJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE, - userId); - } - - private void logUserRemoveJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags, boolean finish) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE, - userId, userType, flags, finish); - } - - private long logGrantAdminJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId); - } - - private void logGrantAdminJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId, userType, flags, true); - } - - private long logRevokeAdminJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId); - } - - private void logRevokeAdminJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId, userType, flags, true); - } - - private void logUserJourneyFinish(long sessionId, int journey, @UserIdInt int userId, - String userType, @UserInfoFlag int flags, boolean finish) { - - // log the journey atom with the user metadata - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, - journey, /* origin_user= */ getCurrentUserId(), userId, - UserManager.getUserTypeForStatsd(userType), flags); - - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, - finish ? FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH - : FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE); - } - - private long logUserJourneyBegin(int journey, @UserIdInt int userId) { - final long sessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); - - // log the event atom to indicate the event start - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN); - return sessionId; - } - - private void logUserJourneyError(long sessionId, int journey, @UserIdInt int userId) { - - // log the journey atom with the user metadata - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, - journey, /* origin_user= */ getCurrentUserId(), userId); - - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__ERROR); - } - /** Register callbacks for statsd pulled atoms. */ private void registerStatsCallbacks() { final StatsManager statsManager = mContext.getSystemService(StatsManager.class); @@ -5352,7 +5246,8 @@ public class UserManagerService extends IUserManager.Stub { if (size > 1) { for (int idx = 0; idx < size; idx++) { final UserInfo user = users.get(idx); - final int userTypeStandard = UserManager.getUserTypeForStatsd(user.userType); + final int userTypeStandard = mUserJourneyLogger + .getUserTypeForStatsd(user.userType); final String userTypeCustom = (userTypeStandard == FrameworkStatsLog .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN) ? @@ -5635,7 +5530,7 @@ public class UserManagerService extends IUserManager.Stub { writeUserLP(userData); } - final long sessionId = logUserRemoveJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_USER_REMOVE); try { mAppOpsService.removeUser(userId); @@ -5657,13 +5552,17 @@ public class UserManagerService extends IUserManager.Stub { @Override public void userStopped(int userIdParam) { finishRemoveUser(userIdParam); - logUserRemoveJourneyFinish(sessionId, userIdParam, - userData.info.userType, userData.info.flags, true); + int originUserId = UserManagerService.this.getCurrentUserId(); + mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_REMOVE, + ERROR_CODE_UNSPECIFIED); } @Override public void userStopAborted(int userIdParam) { - logUserRemoveJourneyFinish(sessionId, userIdParam, - userData.info.userType, userData.info.flags, false); + int originUserId = UserManagerService.this.getCurrentUserId(); + mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_REMOVE, + ERROR_CODE_ABORTED); } }); } catch (RemoteException e) { @@ -7297,9 +7196,9 @@ public class UserManagerService extends IUserManager.Stub { final UserInfo userInfo = getUserInfo(userIds[i]); if (userInfo == null) { // Not possible because the input user ids should all be valid - userTypes[i] = UserManager.getUserTypeForStatsd(""); + userTypes[i] = mUserJourneyLogger.getUserTypeForStatsd(""); } else { - userTypes[i] = UserManager.getUserTypeForStatsd(userInfo.userType); + userTypes[i] = mUserJourneyLogger.getUserTypeForStatsd(userInfo.userType); } } return userTypes; @@ -7536,4 +7435,11 @@ public class UserManagerService extends IUserManager.Stub { .getBoolean(R.bool.config_canSwitchToHeadlessSystemUser); } + /** + * Returns instance of {@link com.android.server.pm.UserJourneyLogger}. + */ + public UserJourneyLogger getUserJourneyLogger() { + return mUserJourneyLogger; + } + } 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 d12741ac8bd6..cbc7dc2f2fe5 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -100,6 +100,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.FgThread; import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; +import com.android.server.pm.UserJourneyLogger; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.wm.WindowManagerService; @@ -1074,6 +1075,8 @@ public class UserControllerTest { private final KeyguardManager mKeyguardManagerMock; private final LockPatternUtils mLockPatternUtilsMock; + private final UserJourneyLogger mUserJourneyLoggerMock; + private final Context mCtx; TestInjector(Context ctx) { @@ -1090,6 +1093,7 @@ public class UserControllerTest { mKeyguardManagerMock = mock(KeyguardManager.class); when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true); mLockPatternUtilsMock = mock(LockPatternUtils.class); + mUserJourneyLoggerMock = mock(UserJourneyLogger.class); } @Override @@ -1220,6 +1224,11 @@ public class UserControllerTest { void onSystemUserVisibilityChanged(boolean visible) { Log.i(TAG, "onSystemUserVisibilityChanged(" + visible + ")"); } + + @Override + protected UserJourneyLogger getUserJourneyLogger() { + return mUserJourneyLoggerMock; + } } private static class TestHandler extends Handler { diff --git a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java new file mode 100644 index 000000000000..20e2692cb747 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_INCOMPLETE_OR_TIMEOUT; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_NULL_USER_INFO; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_UNSPECIFIED; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_BEGIN; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_CANCEL; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_ERROR; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_CREATE_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_REMOVE_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_REVOKE_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_START_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_STOP_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.pm.UserInfo; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class UserJourneyLoggerTest { + + public static final int FULL_USER_ADMIN_FLAG = 0x00000402; + private UserJourneyLogger mUserJourneyLogger; + + @Before + public void setup() throws Exception { + mUserJourneyLogger = spy(new UserJourneyLogger()); + } + + @Test + public void testUserStartLifecycleJourneyReported() { + final UserLifecycleJourneyReportedCaptor report1 = new UserLifecycleJourneyReportedCaptor(); + final UserJourneyLogger.UserJourneySession session = new UserJourneyLogger + .UserJourneySession(10, USER_JOURNEY_USER_START); + + report1.captureLogAndAssert(mUserJourneyLogger, session, + USER_JOURNEY_USER_START, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, 1, + ERROR_CODE_UNSPECIFIED); + } + + + @Test + public void testUserLifecycleEventOccurred() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = new UserJourneyLogger + .UserJourneySession(10, USER_JOURNEY_USER_START); + + report1.captureLogAndAssert(mUserJourneyLogger, session, 0, + USER_LIFECYCLE_EVENT_START_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED); + } + + @Test + public void testLogUserLifecycleEvent() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserLifecycleEvent(10, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testCreateUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(-1, USER_JOURNEY_USER_CREATE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, -1, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserCreateJourneyFinish(0, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_CREATE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testRemoveUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_REMOVE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REMOVE_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_REMOVE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REMOVE_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_REMOVE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testStartUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_START, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testAbortStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + + mUserJourneyLogger.logUserJourneyFinishWithError(-1, targetUser, + USER_JOURNEY_USER_STOP, ERROR_CODE_ABORTED); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_CANCEL, ERROR_CODE_ABORTED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_ABORTED, 1); + } + + @Test + public void testIncompleteStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.finishAndClearIncompleteUserJourney(10, USER_JOURNEY_USER_STOP); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_ERROR, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN, + -1, // information about user are incomplete + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, 1); + } + + @Test + public void testGrantAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_GRANT_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testNullUserErrorGrantAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + + UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logNullUserJourneyError(USER_JOURNEY_GRANT_ADMIN, + 0, 10, "", -1); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_ERROR, ERROR_CODE_NULL_USER_INFO, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_GRANT_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN, + -1, ERROR_CODE_NULL_USER_INFO, 1); + } + + @Test + public void testRevokeAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000400, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testSwitchFGUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 3); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testSwitchUIUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + UserInfo targetUser = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 3); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, + ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testSwitchWithStopUIUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + + // BEGIN USER SWITCH + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + // BEGIN USER STOP + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + // BEGIN USER START + UserJourneyLogger.UserJourneySession session3 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session3.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 3); + + + // FINISH USER STOP + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(-1, targetUser, + USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + FULL_USER_ADMIN_FLAG, ERROR_CODE_UNSPECIFIED, 1); + + // FINISH USER START + final UserInfo targetUser2 = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser2, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session3.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 5); + + report2.captureAndAssert(mUserJourneyLogger, session3.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + FULL_USER_ADMIN_FLAG, ERROR_CODE_UNSPECIFIED, 2); + + + // FINISH USER SWITCH + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser2); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 6); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 3); + } + + static class UserLifecycleJourneyReportedCaptor { + ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<Integer> mJourney = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mOriginalUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mTargetUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mUserType = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mUserFlags = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + + public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, + long sessionId, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode, int times) { + verify(mUserJourneyLogger, times(times)) + .writeUserLifecycleJourneyReported(mSessionId.capture(), + mJourney.capture(), + mOriginalUserId.capture(), + mTargetUserId.capture(), + mUserType.capture(), + mUserFlags.capture(), + mErrorCode.capture()); + + assertThat(mSessionId.getValue()).isEqualTo(sessionId); + assertThat(mJourney.getValue()).isEqualTo(journey); + assertThat(mOriginalUserId.getValue()).isEqualTo(originalUserId); + assertThat(mTargetUserId.getValue()).isEqualTo(targetUserId); + assertThat(mUserType.getValue()).isEqualTo(userType); + assertThat(mUserFlags.getValue()).isEqualTo(userFlags); + assertThat(mErrorCode.getValue()).isEqualTo(errorCode); + } + + + public void captureLogAndAssert(UserJourneyLogger mUserJourneyLogger, + UserJourneyLogger.UserJourneySession session, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode) { + mUserJourneyLogger.logUserLifecycleJourneyReported(session, journey, originalUserId, + targetUserId, userType, userFlags, errorCode); + + captureAndAssert(mUserJourneyLogger, session.mSessionId, journey, originalUserId, + targetUserId, userType, userFlags, errorCode, 1); + } + } + + + static class UserLifecycleEventOccurredCaptor { + ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<Integer> mTargetUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mEvent = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mStste = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + + + public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, + long sessionId, int targetUserId, int event, int state, int errorCode, int times) { + verify(mUserJourneyLogger, times(times)) + .writeUserLifecycleEventOccurred(mSessionId.capture(), + mTargetUserId.capture(), + mEvent.capture(), + mStste.capture(), + mErrorCode.capture()); + + assertThat(mSessionId.getValue()).isEqualTo(sessionId); + assertThat(mTargetUserId.getValue()).isEqualTo(targetUserId); + assertThat(mEvent.getValue()).isEqualTo(event); + assertThat(mStste.getValue()).isEqualTo(state); + assertThat(mErrorCode.getValue()).isEqualTo(errorCode); + } + + + public void captureLogAndAssert(UserJourneyLogger mUserJourneyLogger, + UserJourneyLogger.UserJourneySession session, int targetUserId, int event, + int state, int errorCode) { + mUserJourneyLogger.logUserLifecycleEventOccurred(session, targetUserId, event, + state, errorCode); + + captureAndAssert(mUserJourneyLogger, session.mSessionId, targetUserId, event, + state, errorCode, 1); + } + } +} |