/*
 * Copyright (C) 2017 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.internal.util;

import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Trace.TRACE_TAG_APP;
import static android.provider.DeviceConfig.NAMESPACE_LATENCY_TRACKER;

import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TURN_ON_SCREEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__UNKNOWN_ACTION;
import static com.android.internal.util.LatencyTracker.ActionProperties.ENABLE_SUFFIX;
import static com.android.internal.util.LatencyTracker.ActionProperties.LEGACY_TRACE_THRESHOLD_SUFFIX;
import static com.android.internal.util.LatencyTracker.ActionProperties.SAMPLE_INTERVAL_SUFFIX;
import static com.android.internal.util.LatencyTracker.ActionProperties.TRACE_THRESHOLD_SUFFIX;

import android.Manifest;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityThread;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import android.os.Trace;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.EventLogTags;
import com.android.internal.os.BackgroundThread;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * Class to track various latencies in SystemUI. It then writes the latency to statsd and also
 * outputs it to logcat so these latencies can be captured by tests and then used for dashboards.
 * <p>
 * This is currently only in Keyguard. It can be shared between SystemUI and Keyguard, but
 * eventually we'd want to merge these two packages together so Keyguard can use common classes
 * that are shared with SystemUI.
 */
public class LatencyTracker {
    private static final String TAG = "LatencyTracker";
    public static final String SETTINGS_ENABLED_KEY = "enabled";
    private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
    private static final boolean DEBUG = false;
    /** Default to being enabled on debug builds. */
    private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
    /** Default to collecting data for 1/5 of all actions (randomly sampled). */
    private static final int DEFAULT_SAMPLING_INTERVAL = 5;

    /**
     * Time it takes until the first frame of the notification panel to be displayed while expanding
     */
    public static final int ACTION_EXPAND_PANEL = 0;

    /**
     * Time it takes until the first frame of recents is drawn after invoking it with the button.
     */
    public static final int ACTION_TOGGLE_RECENTS = 1;

    /**
     * Time between we get a fingerprint acquired signal until we start with the unlock animation
     */
    public static final int ACTION_FINGERPRINT_WAKE_AND_UNLOCK = 2;

    /**
     * Time it takes to check PIN/Pattern/Password.
     */
    public static final int ACTION_CHECK_CREDENTIAL = 3;

    /**
     * Time it takes to check fully PIN/Pattern/Password, i.e. that's the time spent including the
     * actions to unlock a user.
     */
    public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4;

    /**
     * Time it takes to turn on the screen.
     */
    public static final int ACTION_TURN_ON_SCREEN = 5;

    /**
     * Time it takes to rotate the screen.
     */
    public static final int ACTION_ROTATE_SCREEN = 6;

    /*
     * Time between we get a face acquired signal until we start with the unlock animation
     */
    public static final int ACTION_FACE_WAKE_AND_UNLOCK = 7;

    /**
     * Time between the swipe-up gesture and window drawn of recents activity.
     */
    public static final int ACTION_START_RECENTS_ANIMATION = 8;

    /**
     * Time it takes to for the camera based algorithm to rotate the screen.
     */
    public static final int ACTION_ROTATE_SCREEN_CAMERA_CHECK = 9;

    /**
     * Time it takes the sensor to detect rotation.
     */
    public static final int ACTION_ROTATE_SCREEN_SENSOR = 10;

    /**
     * Time it takes to start unlock animation .
     */
    public static final int ACTION_LOCKSCREEN_UNLOCK = 11;

    /**
     * Time it takes to switch users.
     */
    public static final int ACTION_USER_SWITCH = 12;

    /**
     * Time it takes to turn on the inner screen for a foldable device.
     */
    public static final int ACTION_SWITCH_DISPLAY_UNFOLD = 13;

    /**
     * Time it takes for a UDFPS sensor to appear ready after it is touched.
     */
    public static final int ACTION_UDFPS_ILLUMINATE = 14;

    /**
     * Time it takes for the gesture back affordance arrow to show up.
     */
    public static final int ACTION_SHOW_BACK_ARROW = 15;

    /**
     * Time it takes for loading share sheet.
     */
    public static final int ACTION_LOAD_SHARE_SHEET = 16;

    /**
     * Time it takes for showing the selection toolbar.
     */
    public static final int ACTION_SHOW_SELECTION_TOOLBAR = 17;

    /**
     * Time it takes to show AOD display after folding the device.
     */
    public static final int ACTION_FOLD_TO_AOD = 18;

    /**
     * Time it takes to show the {@link android.service.voice.VoiceInteractionSession} system UI
     * after a {@link android.hardware.soundtrigger3.ISoundTriggerHw} voice trigger.
     */
    public static final int ACTION_SHOW_VOICE_INTERACTION = 19;

    /**
     * Time it takes to request IME shown animation.
     */
    public static final int ACTION_REQUEST_IME_SHOWN = 20;

    /**
     * Time it takes to request IME hidden animation.
     */
    public static final int ACTION_REQUEST_IME_HIDDEN = 21;

    /**
     * Time it takes to load the animation frames in smart space doorbell card.
     * It measures the duration from the images uris are passed into the view
     * to all the frames are loaded.
     * <p/>
     * A long latency makes the doorbell animation looks janky until all the frames are loaded.
     */
    public static final int ACTION_SMARTSPACE_DOORBELL = 22;

    /**
     * Time it takes to lazy-load the image of a {@link android.app.Notification.BigPictureStyle}
     * notification.
     */
    public static final int ACTION_NOTIFICATION_BIG_PICTURE_LOADED = 23;

    /**
     * Time it takes to unlock the device via udfps, until the whole launcher appears.
     */
    public static final int ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME = 24;

    /**
     * Time it takes to start back preview surface animation after a back gesture starts.
     */
    public static final int ACTION_BACK_SYSTEM_ANIMATION = 25;

    /**
     * Time notifications spent in hidden state for performance reasons. We might temporary
     * hide notifications after display size changes (e.g. fold/unfold of a foldable device)
     * and measure them while they are hidden to unblock rendering of the rest of the UI.
     */
    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE = 26;

    /**
     * The same as {@link ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE} but tracks time only
     * when the notifications are hidden and when the shade is open or keyguard is visible.
     */
    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN = 27;

    private static final int[] ACTIONS_ALL = {
        ACTION_EXPAND_PANEL,
        ACTION_TOGGLE_RECENTS,
        ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
        ACTION_CHECK_CREDENTIAL,
        ACTION_CHECK_CREDENTIAL_UNLOCKED,
        ACTION_TURN_ON_SCREEN,
        ACTION_ROTATE_SCREEN,
        ACTION_FACE_WAKE_AND_UNLOCK,
        ACTION_START_RECENTS_ANIMATION,
        ACTION_ROTATE_SCREEN_CAMERA_CHECK,
        ACTION_ROTATE_SCREEN_SENSOR,
        ACTION_LOCKSCREEN_UNLOCK,
        ACTION_USER_SWITCH,
        ACTION_SWITCH_DISPLAY_UNFOLD,
        ACTION_UDFPS_ILLUMINATE,
        ACTION_SHOW_BACK_ARROW,
        ACTION_LOAD_SHARE_SHEET,
        ACTION_SHOW_SELECTION_TOOLBAR,
        ACTION_FOLD_TO_AOD,
        ACTION_SHOW_VOICE_INTERACTION,
        ACTION_REQUEST_IME_SHOWN,
        ACTION_REQUEST_IME_HIDDEN,
        ACTION_SMARTSPACE_DOORBELL,
        ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
        ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
        ACTION_BACK_SYSTEM_ANIMATION,
        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
    };

    /** @hide */
    @IntDef({
        ACTION_EXPAND_PANEL,
        ACTION_TOGGLE_RECENTS,
        ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
        ACTION_CHECK_CREDENTIAL,
        ACTION_CHECK_CREDENTIAL_UNLOCKED,
        ACTION_TURN_ON_SCREEN,
        ACTION_ROTATE_SCREEN,
        ACTION_FACE_WAKE_AND_UNLOCK,
        ACTION_START_RECENTS_ANIMATION,
        ACTION_ROTATE_SCREEN_CAMERA_CHECK,
        ACTION_ROTATE_SCREEN_SENSOR,
        ACTION_LOCKSCREEN_UNLOCK,
        ACTION_USER_SWITCH,
        ACTION_SWITCH_DISPLAY_UNFOLD,
        ACTION_UDFPS_ILLUMINATE,
        ACTION_SHOW_BACK_ARROW,
        ACTION_LOAD_SHARE_SHEET,
        ACTION_SHOW_SELECTION_TOOLBAR,
        ACTION_FOLD_TO_AOD,
        ACTION_SHOW_VOICE_INTERACTION,
        ACTION_REQUEST_IME_SHOWN,
        ACTION_REQUEST_IME_HIDDEN,
        ACTION_SMARTSPACE_DOORBELL,
        ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
        ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
        ACTION_BACK_SYSTEM_ANIMATION,
        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Action {
    }

    @VisibleForTesting
    public static final int[] STATSD_ACTION = new int[] {
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_TURN_ON_SCREEN,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
    };

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final SparseArray<Session> mSessions = new SparseArray<>();
    @GuardedBy("mLock")
    private final SparseArray<ActionProperties> mActionPropertiesMap = new SparseArray<>();
    @GuardedBy("mLock")
    private boolean mEnabled;
    private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener =
            this::updateProperties;

    // Wrapping this in a holder class achieves lazy loading behavior
    private static final class SLatencyTrackerHolder {
        private static final LatencyTracker sLatencyTracker;

        static {
            sLatencyTracker = new LatencyTracker();
            sLatencyTracker.startListeningForLatencyTrackerConfigChanges();
        }
    }

    public static LatencyTracker getInstance(Context context) {
        return SLatencyTrackerHolder.sLatencyTracker;
    }

    /**
     * Constructor for LatencyTracker
     *
     * <p>This constructor is only visible for test classes to inject their own consumer callbacks
     *
     * @param startListeningForPropertyChanges If set, constructor will register for device config
     *                      property updates prior to returning. If not set,
     *                      {@link #startListeningForLatencyTrackerConfigChanges} must be called
     *                      to start listening.
     */
    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
    @VisibleForTesting
    public LatencyTracker() {
        mEnabled = DEFAULT_ENABLED;
    }

    private void updateProperties(DeviceConfig.Properties properties) {
        synchronized (mLock) {
            int samplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
                    DEFAULT_SAMPLING_INTERVAL);
            boolean wasEnabled = mEnabled;
            mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
            if (wasEnabled != mEnabled) {
                Log.d(TAG, "Latency tracker " + (mEnabled ? "enabled" : "disabled") + ".");
            }
            for (int action : ACTIONS_ALL) {
                String actionName = getNameOfAction(STATSD_ACTION[action]).toLowerCase(Locale.ROOT);
                int legacyActionTraceThreshold = properties.getInt(
                        actionName + LEGACY_TRACE_THRESHOLD_SUFFIX, -1);
                mActionPropertiesMap.put(action, new ActionProperties(action,
                        properties.getBoolean(actionName + ENABLE_SUFFIX, mEnabled),
                        properties.getInt(actionName + SAMPLE_INTERVAL_SUFFIX, samplingInterval),
                        properties.getInt(actionName + TRACE_THRESHOLD_SUFFIX,
                                legacyActionTraceThreshold)));
            }
            onDeviceConfigPropertiesUpdated(mActionPropertiesMap);
        }
    }

    /**
     * Test method to start listening to {@link DeviceConfig} properties changes.
     *
     * <p>During testing, a {@link LatencyTracker} it is desired to stop and start listening for
     * config updates.
     *
     * <p>This is not used for production usages of this class outside of testing as we are
     * using a single static object.
     */
    @VisibleForTesting
    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
    public void startListeningForLatencyTrackerConfigChanges() {
        final Context context = ActivityThread.currentApplication();
        if (context == null) {
            if (DEBUG) {
                Log.d(TAG, "No application for package: " + ActivityThread.currentPackageName());
            }
            return;
        }
        if (context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
            if (DEBUG) {
                synchronized (mLock) {
                    Log.d(TAG, "Initialized the LatencyTracker."
                            + " (No READ_DEVICE_CONFIG permission to change configs)"
                            + " enabled=" + mEnabled + ", package=" + context.getPackageName());
                }
            }
            return;
        }

        // Post initialization to the background in case we're running on the main thread.
        BackgroundThread.getHandler().post(() -> {
            try {
                this.updateProperties(
                        DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER));
                DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER,
                        BackgroundThread.getExecutor(), mOnPropertiesChangedListener);
            } catch (SecurityException ex) {
                // In case of running tests that the main thread passes the check,
                // but the background thread doesn't have necessary permissions.
                // Swallow it since it's ok to ignore device config changes in the tests.
                Log.d(TAG, "Can't get properties: READ_DEVICE_CONFIG granted="
                        + context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
                        + ", package=" + context.getPackageName());
            }
        });
    }

    /**
     * Test method to stop listening to {@link DeviceConfig} properties changes.
     *
     * <p>During testing, a {@link LatencyTracker} it is desired to stop and start listening for
     * config updates.
     *
     * <p>This is not used for production usages of this class outside of testing as we are
     * using a single static object.
     */
    @VisibleForTesting
    public void stopListeningForLatencyTrackerConfigChanges() {
        DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
    }

    /**
     * A helper method to translate action type to name.
     *
     * @param atomsProtoAction the action type defined in AtomsProto.java
     * @return the name of the action
     */
    public static String getNameOfAction(int atomsProtoAction) {
        // Defined in AtomsProto.java
        switch (atomsProtoAction) {
            case UIACTION_LATENCY_REPORTED__ACTION__UNKNOWN_ACTION:
                return "UNKNOWN";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL:
                return "ACTION_EXPAND_PANEL";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS:
                return "ACTION_TOGGLE_RECENTS";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK:
                return "ACTION_FINGERPRINT_WAKE_AND_UNLOCK";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL:
                return "ACTION_CHECK_CREDENTIAL";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED:
                return "ACTION_CHECK_CREDENTIAL_UNLOCKED";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_TURN_ON_SCREEN:
                return "ACTION_TURN_ON_SCREEN";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN:
                return "ACTION_ROTATE_SCREEN";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK:
                return "ACTION_FACE_WAKE_AND_UNLOCK";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION:
                return "ACTION_START_RECENTS_ANIMATION";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK:
                return "ACTION_ROTATE_SCREEN_CAMERA_CHECK";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR:
                return "ACTION_ROTATE_SCREEN_SENSOR";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK:
                return "ACTION_LOCKSCREEN_UNLOCK";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH:
                return "ACTION_USER_SWITCH";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD:
                return "ACTION_SWITCH_DISPLAY_UNFOLD";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE:
                return "ACTION_UDFPS_ILLUMINATE";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW:
                return "ACTION_SHOW_BACK_ARROW";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET:
                return "ACTION_LOAD_SHARE_SHEET";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR:
                return "ACTION_SHOW_SELECTION_TOOLBAR";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD:
                return "ACTION_FOLD_TO_AOD";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION:
                return "ACTION_SHOW_VOICE_INTERACTION";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN:
                return "ACTION_REQUEST_IME_SHOWN";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN:
                return "ACTION_REQUEST_IME_HIDDEN";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL:
                return "ACTION_SMARTSPACE_DOORBELL";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED:
                return "ACTION_NOTIFICATION_BIG_PICTURE_LOADED";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME:
                return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION:
                return "ACTION_BACK_SYSTEM_ANIMATION";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE:
                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE";
            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN:
                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN";
            default:
                throw new IllegalArgumentException("Invalid action");
        }
    }

    private static String getTraceNameOfAction(@Action int action, String tag) {
        if (TextUtils.isEmpty(tag)) {
            return "L<" + getNameOfAction(STATSD_ACTION[action]) + ">";
        } else {
            return "L<" + getNameOfAction(STATSD_ACTION[action]) + "::" + tag + ">";
        }
    }

    private static String getTraceTriggerNameForAction(@Action int action) {
        return "com.android.telemetry.latency-tracker-" + getNameOfAction(STATSD_ACTION[action]);
    }

    /**
     * @deprecated Use {@link #isEnabled(Context, int)}
     */
    @Deprecated
    public static boolean isEnabled(Context ctx) {
        return getInstance(ctx).isEnabled();
    }

    /**
     * @deprecated Used {@link #isEnabled(int)}
     */
    @Deprecated
    public boolean isEnabled() {
        synchronized (mLock) {
            return mEnabled;
        }
    }

    public static boolean isEnabled(Context ctx, int action) {
        return getInstance(ctx).isEnabled(action);
    }

    public boolean isEnabled(int action) {
        synchronized (mLock) {
            ActionProperties actionProperties = mActionPropertiesMap.get(action);
            if (actionProperties != null) {
                return actionProperties.isEnabled();
            }
            return false;
        }
    }

    /**
     * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
     *
     * @param action The action to start. One of the ACTION_* values.
     */
    public void onActionStart(@Action int action) {
        onActionStart(action, null);
    }

    /**
     * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
     *
     * @param action The action to start. One of the ACTION_* values.
     * @param tag The brief description of the action.
     */
    public void onActionStart(@Action int action, String tag) {
        synchronized (mLock) {
            if (!isEnabled(action)) {
                return;
            }
            // skip if the action is already instrumenting.
            if (mSessions.get(action) != null) {
                return;
            }
            Session session = new Session(action, tag);
            session.begin(() -> onActionCancel(action));
            mSessions.put(action, session);

            if (DEBUG) {
                Log.d(TAG, "onActionStart: " + session.name() + ", start=" + session.mStartRtc);
            }
        }
    }

    /**
     * Notifies that an action has ended. <s>This needs to be called from the main thread.</s>
     *
     * @param action The action to end. One of the ACTION_* values.
     */
    public void onActionEnd(@Action int action) {
        synchronized (mLock) {
            if (!isEnabled(action)) {
                return;
            }
            Session session = mSessions.get(action);
            if (session == null) {
                return;
            }
            session.end();
            mSessions.delete(action);
            logAction(action, session.duration());

            if (DEBUG) {
                Log.d(TAG, "onActionEnd:" + session.name() + ", duration=" + session.duration());
            }
        }
    }

    /**
     * Notifies that an action has canceled. <s>This needs to be called from the main thread.</s>
     *
     * @param action The action to cancel. One of the ACTION_* values.
     * @hide
     */
    public void onActionCancel(@Action int action) {
        synchronized (mLock) {
            Session session = mSessions.get(action);
            if (session == null) {
                return;
            }
            session.cancel();
            mSessions.delete(action);

            if (DEBUG) {
                Log.d(TAG, "onActionCancel: " + session.name());
            }
        }
    }

    /**
     * Testing API to get the time when a given action was started.
     *
     * @param action Action which to retrieve start time from
     * @return Elapsed realtime timestamp when the action started. -1 if the action is not active.
     * @hide
     */
    @VisibleForTesting
    @ElapsedRealtimeLong
    public long getActiveActionStartTime(@Action int action) {
        synchronized (mLock) {
            if (mSessions.contains(action)) {
                return mSessions.get(action).mStartRtc;
            }
            return -1;
        }
    }

    /**
     * Logs an action that has started and ended. This needs to be called from the main thread.
     *
     * @param action   The action to end. One of the ACTION_* values.
     * @param duration The duration of the action in ms.
     */
    public void logAction(@Action int action, int duration) {
        boolean shouldSample;
        int traceThreshold;
        synchronized (mLock) {
            if (!isEnabled(action)) {
                return;
            }
            ActionProperties actionProperties = mActionPropertiesMap.get(action);
            if (actionProperties == null) {
                return;
            }
            int nextRandNum = ThreadLocalRandom.current().nextInt(
                    actionProperties.getSamplingInterval());
            shouldSample = nextRandNum == 0;
            traceThreshold = actionProperties.getTraceThreshold();
        }

        boolean shouldTriggerPerfettoTrace = traceThreshold > 0 && duration >= traceThreshold;

        if (DEBUG) {
            Log.i(TAG, "logAction: " + getNameOfAction(STATSD_ACTION[action])
                    + " duration=" + duration
                    + " shouldSample=" + shouldSample
                    + " shouldTriggerPerfettoTrace=" + shouldTriggerPerfettoTrace);
        }

        EventLog.writeEvent(EventLogTags.SYSUI_LATENCY, action, duration);
        if (shouldTriggerPerfettoTrace) {
            onTriggerPerfetto(getTraceTriggerNameForAction(action));
        }
        if (shouldSample) {
            onLogToFrameworkStats(
                    new FrameworkStatsLogEvent(action, FrameworkStatsLog.UI_ACTION_LATENCY_REPORTED,
                            STATSD_ACTION[action], duration)
            );
        }
    }

    static class Session {
        @Action
        private final int mAction;
        private final String mTag;
        private final String mName;
        private Runnable mTimeoutRunnable;
        private long mStartRtc = -1;
        private long mEndRtc = -1;

        Session(@Action int action, @Nullable String tag) {
            mAction = action;
            mTag = tag;
            mName = TextUtils.isEmpty(mTag)
                    ? getNameOfAction(STATSD_ACTION[mAction])
                    : getNameOfAction(STATSD_ACTION[mAction]) + "::" + mTag;
        }

        String name() {
            return mName;
        }

        String traceName() {
            return getTraceNameOfAction(mAction, mTag);
        }

        void begin(@NonNull Runnable timeoutAction) {
            mStartRtc = SystemClock.elapsedRealtime();
            Trace.asyncTraceForTrackBegin(TRACE_TAG_APP, traceName(), traceName(), 0);

            // start counting timeout.
            mTimeoutRunnable = () -> {
                Trace.instantForTrack(TRACE_TAG_APP, traceName(), "timeout");
                timeoutAction.run();
            };
            BackgroundThread.getHandler()
                    .postDelayed(mTimeoutRunnable, TimeUnit.SECONDS.toMillis(15));
        }

        void end() {
            mEndRtc = SystemClock.elapsedRealtime();
            Trace.asyncTraceForTrackEnd(TRACE_TAG_APP, traceName(), 0);
            BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
            mTimeoutRunnable = null;
        }

        void cancel() {
            Trace.instantForTrack(TRACE_TAG_APP, traceName(), "cancel");
            Trace.asyncTraceForTrackEnd(TRACE_TAG_APP, traceName(), 0);
            BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
            mTimeoutRunnable = null;
        }

        int duration() {
            return (int) (mEndRtc - mStartRtc);
        }
    }

    @VisibleForTesting
    public static class ActionProperties {
        static final String ENABLE_SUFFIX = "_enable";
        static final String SAMPLE_INTERVAL_SUFFIX = "_sample_interval";
        // TODO: migrate all usages of the legacy trace threshold property
        static final String LEGACY_TRACE_THRESHOLD_SUFFIX = "";
        static final String TRACE_THRESHOLD_SUFFIX = "_trace_threshold";

        @Action
        private final int mAction;
        private final boolean mEnabled;
        private final int mSamplingInterval;
        private final int mTraceThreshold;

        @VisibleForTesting
        public ActionProperties(
                @Action int action,
                boolean enabled,
                int samplingInterval,
                int traceThreshold) {
            this.mAction = action;
            com.android.internal.util.AnnotationValidations.validate(
                    Action.class, null, mAction);
            this.mEnabled = enabled;
            this.mSamplingInterval = samplingInterval;
            this.mTraceThreshold = traceThreshold;
        }

        @VisibleForTesting
        @Action
        public int getAction() {
            return mAction;
        }

        @VisibleForTesting
        public boolean isEnabled() {
            return mEnabled;
        }

        @VisibleForTesting
        public int getSamplingInterval() {
            return mSamplingInterval;
        }

        @VisibleForTesting
        public int getTraceThreshold() {
            return mTraceThreshold;
        }

        @Override
        public String toString() {
            return "ActionProperties{"
                    + " mAction=" + mAction
                    + ", mEnabled=" + mEnabled
                    + ", mSamplingInterval=" + mSamplingInterval
                    + ", mTraceThreshold=" + mTraceThreshold
                    + "}";
        }

        @Override
        public boolean equals(@Nullable Object o) {
            if (this == o) {
                return true;
            }
            if (o == null) {
                return false;
            }
            if (!(o instanceof ActionProperties)) {
                return false;
            }
            ActionProperties that = (ActionProperties) o;
            return mAction == that.mAction
                    && mEnabled == that.mEnabled
                    && mSamplingInterval == that.mSamplingInterval
                    && mTraceThreshold == that.mTraceThreshold;
        }

        @Override
        public int hashCode() {
            int _hash = 1;
            _hash = 31 * _hash + mAction;
            _hash = 31 * _hash + Boolean.hashCode(mEnabled);
            _hash = 31 * _hash + mSamplingInterval;
            _hash = 31 * _hash + mTraceThreshold;
            return _hash;
        }
    }

    /**
     * Testing method intended to be overridden to determine when the LatencyTracker's device
     * properties are updated.
     */
    @VisibleForTesting
    public void onDeviceConfigPropertiesUpdated(SparseArray<ActionProperties> actionProperties) {
        if (DEBUG) {
            Log.d(TAG, "onDeviceConfigPropertiesUpdated: " + actionProperties);
        }
    }

    /**
     * Testing class intended to be overridden to determine when LatencyTracker triggers perfetto.
     */
    @VisibleForTesting
    public void onTriggerPerfetto(String triggerName) {
        if (DEBUG) {
            Log.i(TAG, "onTriggerPerfetto: triggerName=" + triggerName);
        }
        PerfettoTrigger.trigger(triggerName);
    }

    /**
     * Testing method intended to be overridden to determine when LatencyTracker writes to
     * FrameworkStatsLog.
     */
    @VisibleForTesting
    public void onLogToFrameworkStats(FrameworkStatsLogEvent event) {
        if (DEBUG) {
            Log.i(TAG, "onLogToFrameworkStats: event=" + event);
        }
        FrameworkStatsLog.write(event.logCode, event.statsdAction, event.durationMillis);
    }

    /**
     * Testing class intended to reject what should be written to the {@link FrameworkStatsLog}
     *
     * <p>This class is used in {@link #onLogToFrameworkStats(FrameworkStatsLogEvent)} for test code
     * to observer when and what information is being logged by {@link LatencyTracker}
     */
    @VisibleForTesting
    public static class FrameworkStatsLogEvent {

        @VisibleForTesting
        public final int action;
        @VisibleForTesting
        public final int logCode;
        @VisibleForTesting
        public final int statsdAction;
        @VisibleForTesting
        public final int durationMillis;

        private FrameworkStatsLogEvent(int action, int logCode, int statsdAction,
                int durationMillis) {
            this.action = action;
            this.logCode = logCode;
            this.statsdAction = statsdAction;
            this.durationMillis = durationMillis;
        }

        @Override
        public String toString() {
            return "FrameworkStatsLogEvent{"
                    + " logCode=" + logCode
                    + ", statsdAction=" + statsdAction
                    + ", durationMillis=" + durationMillis
                    + "}";
        }
    }
}
