/*
 * 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;
    public static final int ERROR_CODE_INVALID_USER_TYPE = 7;
    public static final int ERROR_CODE_USER_IS_LAST_ADMIN = 8;

    @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,
            ERROR_CODE_INVALID_USER_TYPE,
            ERROR_CODE_USER_IS_LAST_ADMIN,
    })
    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;
    public static final int USER_JOURNEY_DEMOTE_MAIN_USER =
            FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__DEMOTE_MAIN_USER;
    public static final int USER_JOURNEY_PROMOTE_MAIN_USER =
            FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__PROMOTE_MAIN_USER;

    @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,
            USER_JOURNEY_DEMOTE_MAIN_USER,
            USER_JOURNEY_PROMOTE_MAIN_USER
    })
    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;
        }
    }
}