blob: f120763a9e1aa40e63be434bfc0b096be313e72a [file] [log] [blame]
/*
* 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_PROFILE_PRIVATE;
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;
public static final int USER_JOURNEY_USER_LIFECYCLE =
FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_LIFECYCLE;
@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,
USER_JOURNEY_USER_LIFECYCLE
})
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;
case USER_TYPE_PROFILE_PRIVATE:
return FrameworkStatsLog
.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_PRIVATE;
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, -1);
} else {
final long elapsedTime = System.currentTimeMillis() - session.mStartTimeInMills;
writeUserLifecycleJourneyReported(
session.mSessionId, journey, originalUserId, targetUserId, userType, userFlags,
errorCode, elapsedTime);
}
}
/**
* Helper method for spy testing
*/
@VisibleForTesting
public void writeUserLifecycleJourneyReported(long sessionId, int journey, int originalUserId,
int targetUserId, int userType, int userFlags, int errorCode, long elapsedTime) {
FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED,
sessionId, journey, originalUserId, targetUserId, userType, userFlags,
errorCode, elapsedTime);
}
/**
* 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 user journey event and report finishing with error
*/
public UserJourneySession logDelayedUserJourneyFinishWithError(@UserIdInt int originalUserId,
UserInfo targetUser, @UserJourney int journey, @UserJourneyErrorCode int errorCode) {
synchronized (mLock) {
final int key = getUserJourneyKey(targetUser.id, journey);
final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key);
if (userJourneySession != null) {
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;
}
}
/**
* This keeps the start time when finishing extensively long journey was began.
* For instance full user lifecycle ( from creation to deletion )when user is about to delete
* we need to get user creation time before it was deleted.
*/
public UserJourneySession startSessionForDelayedJourney(@UserIdInt int targetId,
@UserJourney int journey, long startTime) {
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, startTime);
mUserIdToUserJourneyMap.append(key, userJourneySession);
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;
public final long mStartTimeInMills;
@VisibleForTesting
public UserJourneySession(long sessionId, @UserJourney int journey) {
mJourney = journey;
mSessionId = sessionId;
mStartTimeInMills = System.currentTimeMillis();
}
@VisibleForTesting
public UserJourneySession(long sessionId, @UserJourney int journey, long startTimeInMills) {
mJourney = journey;
mSessionId = sessionId;
mStartTimeInMills = startTimeInMills;
}
}
}