/*
 * Copyright (C) 2016 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.performance.tests;

import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.loganalysis.item.LatencyItem;
import com.android.loganalysis.item.TransitionDelayItem;
import com.android.loganalysis.parser.EventsLogParser;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.LogcatReceiver;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.ListInstrumentationParser;
import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
import com.android.tradefed.util.SimpleStats;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.ZipUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Test that drives the transition delays during different user behavior like cold launch from
 * launcher, hot launch from recent etc.This class invokes the instrumentation test apk that does
 * the transition and captures the events logs during the transition and parse them and report in
 * the dashboard.
 */
public class AppTransitionTests implements IRemoteTest, IDeviceTest {

    private static final String PACKAGE_NAME = "com.android.apptransition.tests";
    private static final String CLASS_NAME = "com.android.apptransition.tests.AppTransitionTests";
    private static final String TEST_COLD_LAUNCH = "testColdLaunchFromLauncher";
    private static final String TEST_HOT_LAUNCH = "testHotLaunchFromLauncher";
    private static final String TEST_APP_TO_HOME = "testAppToHome";
    private static final String TEST_APP_TO_RECENT = "testAppToRecents";
    private static final String TEST_LATENCY = "testLatency";
    private static final String TEST_HOT_LAUNCH_FROM_RECENTS = "testHotLaunchFromRecents";
    private static final String DROP_CACHE_SCRIPT_FILE = "dropCache";
    private static final String SCRIPT_EXTENSION = ".sh";
    private static final String DROP_CACHE_CMD = "echo 3 > /proc/sys/vm/drop_caches";
    private static final String DEVICE_TEMPORARY_DIR_PATH = "/data/local/tmp/";
    private static final String REMOVE_CMD = "rm -rf %s/%s";
    private static final String EVENTS_CMD = "logcat -v threadtime -b events";
    private static final String EVENTS_CLEAR_CMD = "logcat -v threadtime -b events -c";
    private static final String EVENTS_LOG = "events_log";
    private static final long EVENTS_LOGCAT_SIZE = 80 * 1024 * 1024;

    @Option(
            name = "cold-apps",
            description =
                    "Apps used for cold app launch"
                            + " transition delay testing from launcher screen.")
    private String mColdLaunchApps = null;

    @Option(
            name = "hot-apps",
            description =
                    "Apps used for hot app launch"
                            + " transition delay testing from launcher screen.")
    private String mHotLaunchApps = null;

    @Option(
            name = "pre-launch-apps",
            description =
                    "Apps used for populating the"
                            + " recents apps list before starting app_to_recents or hot_app_from_recents"
                            + " testing.")
    private String mPreLaunchApps = null;

    @Option(
            name = "apps-to-recents",
            description = "Apps used for app to recents" + " transition delay testing.")
    private String mAppToRecents = null;

    @Option(
            name = "hot-apps-from-recents",
            description =
                    "Apps used for hot" + " launch app from recents list transition delay testing.")
    private String mRecentsToApp = null;

    @Option(
            name = "launch-iteration",
            description = "Iterations for launching each app to" + "test the transition delay.")
    private int mLaunchIteration = 10;

    @Option(
            name = "trace-directory",
            description =
                    "Directory to store the trace files"
                            + "while testing ther app transition delay.")
    private String mTraceDirectory = null;

    @Option(name = "runner", description = "The instrumentation test runner class name to use.")
    private String mRunnerName = "";

    @Option(name = "run-arg", description = "Additional test specific arguments to provide.")
    private Map<String, String> mArgMap = new LinkedHashMap<String, String>();

    @Option(name = "launcher-activity", description = "Home activity name")
    private String mLauncherActivity = ".NexusLauncherActivity";

    @Option(
            name = "class",
            description =
                    "test class to run, may be repeated; multiple classess will be run"
                            + " in the same order as provided in command line")
    private List<String> mClasses = new ArrayList<String>();

    @Option(name = "package", description = "The manifest package name of the UI test package")
    private String mPackage = "com.android.apptransition.tests";

    @Option(name = "latency-iteration", description = "Iterations to be used in the latency tests.")
    private int mLatencyIteration = 10;

    @Option(
            name = "timeout",
            description =
                    "Aborts the test run if any test takes longer than the specified number "
                            + "of milliseconds. For no timeout, set to 0.",
            isTimeVal = true)
    private long mTestTimeout = 45 * 60 * 1000; // default to 45 minutes

    @Option(
            name = "isolated-storage",
            description =
                    "If set to false, the '--no-isolated-storage' flag will be passed to the am "
                            + "instrument command. Only works for Q or later."
        )
    private boolean mIsolatedStorage = true;

    private ITestDevice mDevice = null;
    private IRemoteAndroidTestRunner mRunner = null;
    private CollectingTestListener mLaunchListener = null;
    private LogcatReceiver mLaunchEventsLogs = null;
    private EventsLogParser mEventsLogParser = new EventsLogParser();
    private ITestInvocationListener mListener = null;
    private ListInstrumentationParser mListInstrumentationParser = null;

    @Override
    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {

        addDropCacheScriptFile();
        mListener = listener;

        if (null != mColdLaunchApps && !mColdLaunchApps.isEmpty()) {
            try {
                mRunner = createRemoteAndroidTestRunner(TEST_COLD_LAUNCH, mColdLaunchApps, null);
                mLaunchListener = new CollectingTestListener();
                mLaunchEventsLogs =
                        new LogcatReceiver(getDevice(), EVENTS_CMD, EVENTS_LOGCAT_SIZE, 0);
                startEventsLogs(mLaunchEventsLogs, TEST_COLD_LAUNCH);
                runTests();
                analyzeColdLaunchDelay(parseTransitionDelayInfo());
            } finally {
                stopEventsLogs(mLaunchEventsLogs, TEST_COLD_LAUNCH);
                if (isTraceDirEnabled()) {
                    uploadTraceFiles(listener, TEST_COLD_LAUNCH);
                }
            }
        }

        if (null != mHotLaunchApps && !mHotLaunchApps.isEmpty()) {
            try {
                mRunner = createRemoteAndroidTestRunner(TEST_HOT_LAUNCH, mHotLaunchApps, null);
                mLaunchListener = new CollectingTestListener();
                mLaunchEventsLogs =
                        new LogcatReceiver(getDevice(), EVENTS_CMD, EVENTS_LOGCAT_SIZE, 0);
                startEventsLogs(mLaunchEventsLogs, TEST_HOT_LAUNCH);
                runTests();
                analyzeHotLaunchDelay(parseTransitionDelayInfo());
            } finally {
                stopEventsLogs(mLaunchEventsLogs, TEST_HOT_LAUNCH);
                if (isTraceDirEnabled()) {
                    uploadTraceFiles(listener, TEST_HOT_LAUNCH);
                }
            }
        }

        if ((null != mAppToRecents && !mAppToRecents.isEmpty())
                && (null != mPreLaunchApps && !mPreLaunchApps.isEmpty())) {
            try {
                mRunner =
                        createRemoteAndroidTestRunner(
                                TEST_APP_TO_RECENT, mAppToRecents, mPreLaunchApps);
                mLaunchListener = new CollectingTestListener();
                mLaunchEventsLogs =
                        new LogcatReceiver(getDevice(), EVENTS_CMD, EVENTS_LOGCAT_SIZE, 0);
                startEventsLogs(mLaunchEventsLogs, TEST_APP_TO_RECENT);
                runTests();
                analyzeAppToRecentsDelay(parseTransitionDelayInfo());
            } finally {
                stopEventsLogs(mLaunchEventsLogs, TEST_APP_TO_RECENT);
                if (isTraceDirEnabled()) {
                    uploadTraceFiles(listener, TEST_APP_TO_RECENT);
                }
            }
        }

        if ((null != mRecentsToApp && !mRecentsToApp.isEmpty())
                && (null != mPreLaunchApps && !mPreLaunchApps.isEmpty())) {
            try {
                mRunner =
                        createRemoteAndroidTestRunner(
                                TEST_HOT_LAUNCH_FROM_RECENTS, mRecentsToApp, mPreLaunchApps);
                mLaunchListener = new CollectingTestListener();
                mLaunchEventsLogs =
                        new LogcatReceiver(getDevice(), EVENTS_CMD, EVENTS_LOGCAT_SIZE, 0);
                startEventsLogs(mLaunchEventsLogs, TEST_HOT_LAUNCH_FROM_RECENTS);
                runTests();
                analyzeRecentsToAppDelay(parseTransitionDelayInfo());
            } finally {
                stopEventsLogs(mLaunchEventsLogs, TEST_HOT_LAUNCH_FROM_RECENTS);
                if (isTraceDirEnabled()) {
                    uploadTraceFiles(listener, TEST_HOT_LAUNCH_FROM_RECENTS);
                }
            }
        }

        if (!mClasses.isEmpty()) {
            try {
                mRunner = createTestRunner();
                mLaunchListener = new CollectingTestListener();
                mLaunchEventsLogs =
                        new LogcatReceiver(getDevice(), EVENTS_CMD, EVENTS_LOGCAT_SIZE, 0);
                startEventsLogs(mLaunchEventsLogs, TEST_LATENCY);
                runTests();
                analyzeLatencyInfo(parseLatencyInfo());
            } finally {
                stopEventsLogs(mLaunchEventsLogs, TEST_LATENCY);
                if (isTraceDirEnabled()) {
                    uploadTraceFiles(listener, TEST_LATENCY);
                }
            }
        }
    }

    private void runTests() throws DeviceNotAvailableException {
        mDevice.runInstrumentationTests(mRunner, mLaunchListener);
        final TestRunResult runResults = mLaunchListener.getCurrentRunResults();
        if (runResults.isRunFailure()) {
            throw new RuntimeException("Error: test run failed!");
        }
        if (runResults.hasFailedTests()) {
            throw new RuntimeException("Error: some tests failed!");
        }
    }

    /**
     * Push drop cache script file to test device used for clearing the cache between the app
     * launches.
     *
     * @throws DeviceNotAvailableException
     */
    private void addDropCacheScriptFile() throws DeviceNotAvailableException {
        File scriptFile = null;
        try {
            scriptFile = FileUtil.createTempFile(DROP_CACHE_SCRIPT_FILE, SCRIPT_EXTENSION);
            FileUtil.writeToFile(DROP_CACHE_CMD, scriptFile);
            getDevice()
                    .pushFile(
                            scriptFile,
                            String.format(
                                    "%s%s.sh", DEVICE_TEMPORARY_DIR_PATH, DROP_CACHE_SCRIPT_FILE));
        } catch (IOException ioe) {
            CLog.e("Unable to create the Script file");
            CLog.e(ioe);
        }
        getDevice()
                .executeShellCommand(
                        String.format(
                                "chmod 755 %s%s.sh",
                                DEVICE_TEMPORARY_DIR_PATH, DROP_CACHE_SCRIPT_FILE));
        scriptFile.delete();
    }

    /**
     * Method to create the runner with given list of arguments
     *
     * @return the {@link IRemoteAndroidTestRunner} to use.
     * @throws DeviceNotAvailableException
     */
    IRemoteAndroidTestRunner createRemoteAndroidTestRunner(
            String testName, String launchApps, String preLaunchApps)
            throws DeviceNotAvailableException {
        if(mRunnerName.isEmpty()) {
            mRunnerName = queryRunnerName();
        }
        RemoteAndroidTestRunner runner =
                new RemoteAndroidTestRunner(PACKAGE_NAME, mRunnerName, mDevice.getIDevice());
        runner.setMethodName(CLASS_NAME, testName);
        runner.addInstrumentationArg("launch_apps", launchApps);
        runner.setMaxTimeout(mTestTimeout, TimeUnit.MILLISECONDS);
        if (null != preLaunchApps && !preLaunchApps.isEmpty()) {
            runner.addInstrumentationArg("pre_launch_apps", preLaunchApps);
        }
        runner.addInstrumentationArg("launch_iteration", Integer.toString(mLaunchIteration));
        for (Map.Entry<String, String> entry : getTestRunArgMap().entrySet()) {
            runner.addInstrumentationArg(entry.getKey(), entry.getValue());
        }
        if (isTraceDirEnabled()) {
            mDevice.executeShellCommand(String.format("rm -rf %s/%s", mTraceDirectory, testName));
            runner.addInstrumentationArg("trace_directory", mTraceDirectory);
        }

        String runOptions = "";

        // isolated-storage flag only exists in Q and after.
        if (!mIsolatedStorage && getDevice().checkApiLevelAgainstNextRelease(29)) {
            runOptions += "--no-isolated-storage ";
        }

        runner.setRunOptions(runOptions);

        return runner;
    }

    /**
     * Get the {@link ListInstrumentationParser} used to parse 'pm list instrumentation' queries.
     */
    protected ListInstrumentationParser getListInstrumentationParser() {
        if (mListInstrumentationParser == null) {
            mListInstrumentationParser = new ListInstrumentationParser();
        }
        return mListInstrumentationParser;
    }

    /**
     * Query the device for a test runner to use.
     *
     * @return the first test runner name that matches the package or null if we don't find any.
     * @throws DeviceNotAvailableException
     */
    protected String queryRunnerName() throws DeviceNotAvailableException {
        ListInstrumentationParser parser = getListInstrumentationParser();
        getDevice().executeShellCommand("pm list instrumentation", parser);

        for (InstrumentationTarget target : parser.getInstrumentationTargets()) {
            if (PACKAGE_NAME.equals(target.packageName)) {
                return target.runnerName;
            }
        }
        throw new RuntimeException(
                String.format("Unable to determine runner name for package: %s", PACKAGE_NAME));
    }

    /**
     * Method to create the runner with given runner name, package and list of classes.
     *
     * @return the {@link IRemoteAndroidTestRunner} to use.
     * @throws DeviceNotAvailableException
     */
    IRemoteAndroidTestRunner createTestRunner() throws DeviceNotAvailableException {
        IRemoteAndroidTestRunner runner =
                new RemoteAndroidTestRunner(mPackage, mRunnerName, getDevice().getIDevice());
        if (!mClasses.isEmpty()) {
            runner.setClassNames(mClasses.toArray(new String[] {}));
        }
        runner.addInstrumentationArg("iteration_count", Integer.toString(mLatencyIteration));
        for (Map.Entry<String, String> entry : getTestRunArgMap().entrySet()) {
            runner.addInstrumentationArg(entry.getKey(), entry.getValue());
        }
        if (isTraceDirEnabled()) {
            mDevice.executeShellCommand(
                    String.format("rm -rf %s/%s", mTraceDirectory, TEST_LATENCY));
            runner.addInstrumentationArg(
                    "trace_directory", String.format("%s/%s", mTraceDirectory, TEST_LATENCY));
        }
        return runner;
    }

    /**
     * Start the events logcat
     *
     * @param logReceiver
     * @param testName
     * @throws DeviceNotAvailableException
     */
    private void startEventsLogs(LogcatReceiver logReceiver, String testName)
            throws DeviceNotAvailableException {
        getDevice().clearLogcat();
        getDevice().executeShellCommand(EVENTS_CLEAR_CMD);
        logReceiver.start();
    }

    /**
     * Stop the events logcat and upload the data to sponge
     *
     * @param logReceiver
     */
    private void stopEventsLogs(LogcatReceiver logReceiver, String launchDesc) {
        try (InputStreamSource logcatData = logReceiver.getLogcatData()) {
            mListener.testLog(
                    String.format("%s-%s", EVENTS_LOG, launchDesc), LogDataType.TEXT, logcatData);
        } finally {
            logReceiver.stop();
        }
    }

    /**
     * Pull the trace files if exist under destDirectory and log it.
     *
     * @param listener test result listener
     * @param srcDirectory source directory in the device where the files are copied to the local
     *     tmp directory
     * @param subFolderName to store the files corresponding to the test
     * @throws DeviceNotAvailableException
     * @throws IOException
     */
    private void logTraceFiles(
            ITestInvocationListener listener, String srcDirectory, String subFolderName)
            throws DeviceNotAvailableException, IOException {
        File tmpDestDir = null;
        FileInputStreamSource streamSource = null;
        File zipFile = null;
        try {
            tmpDestDir = FileUtil.createTempDir(subFolderName);
            IFileEntry srcDir =
                    mDevice.getFileEntry(String.format("%s/%s", srcDirectory, subFolderName));
            // Files are retrieved from source directory in device
            if (srcDir != null) {
                for (IFileEntry file : srcDir.getChildren(false)) {
                    File pulledFile = new File(tmpDestDir, file.getName());
                    if (!mDevice.pullFile(file.getFullPath(), pulledFile)) {
                        throw new IOException("Not able to pull the file from test device");
                    }
                }
                zipFile = ZipUtil.createZip(tmpDestDir);
                streamSource = new FileInputStreamSource(zipFile);
                listener.testLog(tmpDestDir.getName(), LogDataType.ZIP, streamSource);
            }
        } finally {
            FileUtil.recursiveDelete(tmpDestDir);
            StreamUtil.cancel(streamSource);
            FileUtil.deleteFile(zipFile);
        }
    }

    /**
     * To upload the trace files stored in the traceDirectory in device to sponge.
     *
     * @param listener
     * @param subFolderName
     * @throws DeviceNotAvailableException
     */
    private void uploadTraceFiles(ITestInvocationListener listener, String subFolderName)
            throws DeviceNotAvailableException {
        try {
            logTraceFiles(listener, mTraceDirectory, subFolderName);
        } catch (IOException ioe) {
            CLog.e("Problem in uploading the log files.");
            CLog.e(ioe);
        }
        mDevice.executeShellCommand(String.format(REMOVE_CMD, mTraceDirectory, subFolderName));
    }

    /** Returns false if the traceDirectory is not set. */
    private boolean isTraceDirEnabled() {
        return (null != mTraceDirectory && !mTraceDirectory.isEmpty());
    }

    /** To parse the transition delay info from the events log. */
    private List<TransitionDelayItem> parseTransitionDelayInfo() {
        List<TransitionDelayItem> transitionDelayItems = null;
        try (InputStreamSource logcatData = mLaunchEventsLogs.getLogcatData();
                InputStream logcatStream = logcatData.createInputStream();
                InputStreamReader streamReader = new InputStreamReader(logcatStream);
                BufferedReader reader = new BufferedReader(streamReader)) {
            transitionDelayItems = mEventsLogParser.parseTransitionDelayInfo(reader);
        } catch (IOException e) {
            CLog.e("Problem in parsing the transition delay items from events log");
            CLog.e(e);
        }
        return transitionDelayItems;
    }

    /** To parse the latency info from the events log. */
    private List<LatencyItem> parseLatencyInfo() {
        List<LatencyItem> latencyItems = null;
        try (InputStreamSource logcatData = mLaunchEventsLogs.getLogcatData();
                InputStream logcatStream = logcatData.createInputStream();
                InputStreamReader streamReader = new InputStreamReader(logcatStream);
                BufferedReader reader = new BufferedReader(streamReader)) {
            latencyItems = mEventsLogParser.parseLatencyInfo(reader);
        } catch (IOException e) {
            CLog.e("Problem in parsing the latency items from events log");
            CLog.e(e);
        }
        return latencyItems;
    }

    /**
     * Analyze and report the cold launch transition delay from launcher screen.
     *
     * @param transitionDelayItems
     */
    private void analyzeColdLaunchDelay(List<TransitionDelayItem> transitionDelayItems) {
        Map<String, String> cmpNameAppMap = reverseAppCmpInfoMap(getAppComponentInfoMap());
        Map<String, List<Long>> appKeyTransitionDelayMap = new HashMap<>();
        // Handle launcher to cold app launch transition
        for (TransitionDelayItem delayItem : transitionDelayItems) {
            if (cmpNameAppMap.containsKey(delayItem.getComponentName())) {
                String appName = cmpNameAppMap.get(delayItem.getComponentName());
                if (delayItem.getStartingWindowDelay() != null) {
                    if (appKeyTransitionDelayMap.containsKey(appName)) {
                        appKeyTransitionDelayMap
                                .get(appName)
                                .add(delayItem.getStartingWindowDelay());
                    } else {
                        List<Long> delayTimeList = new ArrayList<Long>();
                        delayTimeList.add(delayItem.getStartingWindowDelay());
                        appKeyTransitionDelayMap.put(appName, delayTimeList);
                    }
                }
            }
        }
        removeAdditionalLaunchInfo(appKeyTransitionDelayMap);
        computeAndUploadResults(TEST_COLD_LAUNCH, appKeyTransitionDelayMap);
    }

    /**
     * Analyze and report the hot launch transition delay from launcher and app to home transition
     * delay. Keep track of launcher to app transition delay which immediately followed by app to
     * home transition. Skip the initial cold launch on the apps.
     *
     * @param transitionDelayItems
     */
    private void analyzeHotLaunchDelay(List<TransitionDelayItem> transitionDelayItems) {
        Map<String, String> cmpNameAppMap = reverseAppCmpInfoMap(getAppComponentInfoMap());
        Map<String, List<Long>> appKeyTransitionDelayMap = new HashMap<>();
        Map<String, List<Long>> appToHomeKeyTransitionDelayMap = new HashMap<>();
        String prevAppName = null;
        for (TransitionDelayItem delayItem : transitionDelayItems) {
            // Handle app to home transition
            if (null != prevAppName) {
                if (delayItem.getComponentName().contains(mLauncherActivity)) {
                    if (appToHomeKeyTransitionDelayMap.containsKey(prevAppName)) {
                        appToHomeKeyTransitionDelayMap
                                .get(prevAppName)
                                .add(delayItem.getWindowDrawnDelay());
                    } else {
                        List<Long> delayTimeList = new ArrayList<Long>();
                        delayTimeList.add(delayItem.getWindowDrawnDelay());
                        appToHomeKeyTransitionDelayMap.put(prevAppName, delayTimeList);
                    }
                    prevAppName = null;
                }
                continue;
            }
            // Handle launcher to hot app launch transition
            if (cmpNameAppMap.containsKey(delayItem.getComponentName())) {
                // Not to consider the first cold launch for the app.
                if (delayItem.getStartingWindowDelay() != null) {
                    continue;
                }
                String appName = cmpNameAppMap.get(delayItem.getComponentName());
                if (appKeyTransitionDelayMap.containsKey(appName)) {
                    appKeyTransitionDelayMap.get(appName).add(delayItem.getTransitionDelay());
                } else {
                    List<Long> delayTimeList = new ArrayList<Long>();
                    delayTimeList.add(delayItem.getTransitionDelay());
                    appKeyTransitionDelayMap.put(appName, delayTimeList);
                }
                prevAppName = appName;
            }
        }
        // Remove the first hot launch info through intents
        removeAdditionalLaunchInfo(appKeyTransitionDelayMap);
        computeAndUploadResults(TEST_HOT_LAUNCH, appKeyTransitionDelayMap);
        removeAdditionalLaunchInfo(appToHomeKeyTransitionDelayMap);
        computeAndUploadResults(TEST_APP_TO_HOME, appToHomeKeyTransitionDelayMap);
    }

    /**
     * Analyze and report app to recents transition delay info.
     *
     * @param transitionDelayItems
     */
    private void analyzeAppToRecentsDelay(List<TransitionDelayItem> transitionDelayItems) {
        Map<String, String> cmpNameAppMap = reverseAppCmpInfoMap(getAppComponentInfoMap());
        Map<String, List<Long>> appKeyTransitionDelayMap = new HashMap<>();
        String prevAppName = null;
        for (TransitionDelayItem delayItem : transitionDelayItems) {
            if (delayItem.getComponentName().contains(mLauncherActivity)) {
                if (appKeyTransitionDelayMap.containsKey(prevAppName)) {
                    appKeyTransitionDelayMap.get(prevAppName).add(delayItem.getWindowDrawnDelay());
                } else {
                    if (null != prevAppName) {
                        List<Long> delayTimeList = new ArrayList<Long>();
                        delayTimeList.add(delayItem.getWindowDrawnDelay());
                        appKeyTransitionDelayMap.put(prevAppName, delayTimeList);
                    }
                }
                prevAppName = null;
                continue;
            }

            if (cmpNameAppMap.containsKey(delayItem.getComponentName())) {
                prevAppName = cmpNameAppMap.get(delayItem.getComponentName());
            }
        }
        // Removing the first cold launch to recents transition delay.
        removeAdditionalLaunchInfo(appKeyTransitionDelayMap);
        computeAndUploadResults(TEST_APP_TO_RECENT, appKeyTransitionDelayMap);
    }

    /**
     * Analyze and report recents to hot app launch delay info. Skip the initial cold launch
     * transition delay on the apps. Also the first launch cannot always be the cold launch because
     * the apps could be part of preapps list. The transition delay is tracked based on recents to
     * apps transition delay items.
     *
     * @param transitionDelayItems
     */
    private void analyzeRecentsToAppDelay(List<TransitionDelayItem> transitionDelayItems) {
        Map<String, String> cmpNameAppMap = reverseAppCmpInfoMap(getAppComponentInfoMap());
        Map<String, List<Long>> appKeyTransitionDelayMap = new HashMap<>();
        boolean isRecentsBefore = false;
        for (TransitionDelayItem delayItem : transitionDelayItems) {
            if (delayItem.getComponentName().contains(mLauncherActivity)) {
                isRecentsBefore = true;
                continue;
            }
            if (isRecentsBefore && cmpNameAppMap.containsKey(delayItem.getComponentName())) {
                if (delayItem.getStartingWindowDelay() != null) {
                    continue;
                }
                String appName = cmpNameAppMap.get(delayItem.getComponentName());
                if (appKeyTransitionDelayMap.containsKey(appName)) {
                    appKeyTransitionDelayMap.get(appName).add(delayItem.getTransitionDelay());
                } else {
                    List<Long> delayTimeList = new ArrayList<Long>();
                    delayTimeList.add(delayItem.getTransitionDelay());
                    appKeyTransitionDelayMap.put(appName, delayTimeList);
                }
            }
            isRecentsBefore = false;
        }
        removeAdditionalLaunchInfo(appKeyTransitionDelayMap);
        computeAndUploadResults(TEST_HOT_LAUNCH_FROM_RECENTS, appKeyTransitionDelayMap);
    }

    /**
     * Analyze and report different latency delay items captured from the events log file while
     * running the LatencyTests
     *
     * @param latencyItemsList
     */
    private void analyzeLatencyInfo(List<LatencyItem> latencyItemsList) {
        Map<String, List<Long>> actionDelayListMap = new HashMap<>();
        for (LatencyItem delayItem : latencyItemsList) {
            if (actionDelayListMap.containsKey(Integer.toString(delayItem.getActionId()))) {
                actionDelayListMap
                        .get(Integer.toString(delayItem.getActionId()))
                        .add(delayItem.getDelay());
            } else {
                List<Long> delayList = new ArrayList<Long>();
                delayList.add(delayItem.getDelay());
                actionDelayListMap.put(Integer.toString(delayItem.getActionId()), delayList);
            }
        }
        computeAndUploadResults(TEST_LATENCY, actionDelayListMap);
    }

    /**
     * To remove the additional launch info at the beginning of the test.
     *
     * @param appKeyTransitionDelayMap
     */
    private void removeAdditionalLaunchInfo(Map<String, List<Long>> appKeyTransitionDelayMap) {
        for (List<Long> delayList : appKeyTransitionDelayMap.values()) {
            while (delayList.size() > mLaunchIteration) {
                delayList.remove(0);
            }
        }
    }

    /**
     * To compute the min,max,avg,median and std_dev on the transition delay for each app and upload
     * the metrics
     *
     * @param reportingKey
     * @param appKeyTransitionDelayMap
     */
    private void computeAndUploadResults(
            String reportingKey, Map<String, List<Long>> appKeyTransitionDelayMap) {
        CLog.i(String.format("Testing : %s", reportingKey));
        Map<String, String> activityMetrics = new HashMap<String, String>();
        for (String appNameKey : appKeyTransitionDelayMap.keySet()) {
            List<Long> delayList = appKeyTransitionDelayMap.get(appNameKey);
            StringBuffer delayListStr = new StringBuffer();
            for (Long delayItem : delayList) {
                delayListStr.append(delayItem);
                delayListStr.append(",");
            }
            CLog.i("%s : %s", appNameKey, delayListStr);
            SimpleStats stats = new SimpleStats();
            for (Long delay : delayList) {
                stats.add(Double.parseDouble(delay.toString()));
            }
            activityMetrics.put(appNameKey + "_min", stats.min().toString());
            activityMetrics.put(appNameKey + "_max", stats.max().toString());
            activityMetrics.put(appNameKey + "_avg", stats.mean().toString());
            activityMetrics.put(appNameKey + "_median", stats.median().toString());
            activityMetrics.put(appNameKey + "_std_dev", stats.stdev().toString());
        }
        mListener.testRunStarted(reportingKey, 0);
        mListener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(activityMetrics));
    }

    /** Retrieve the map of appname,componenetname from the results. */
    private Map<String, String> getAppComponentInfoMap() {
        Collection<TestResult> testResultsCollection =
                mLaunchListener.getCurrentRunResults().getTestResults().values();
        List<TestResult> testResults = new ArrayList<>(testResultsCollection);
        return testResults.get(0).getMetrics();
    }

    /**
     * Reverse and return the given appName,componentName info map to componenetName,appName info
     * map.
     */
    private Map<String, String> reverseAppCmpInfoMap(Map<String, String> appNameCmpNameMap) {
        Map<String, String> cmpNameAppNameMap = new HashMap<String, String>();
        for (Map.Entry<String, String> entry : appNameCmpNameMap.entrySet()) {
            cmpNameAppNameMap.put(entry.getValue(), entry.getKey());
        }
        return cmpNameAppNameMap;
    }

    @Override
    public void setDevice(ITestDevice device) {
        mDevice = device;
    }

    @Override
    public ITestDevice getDevice() {
        return mDevice;
    }

    /** @return the arguments map to pass to the test runner. */
    public Map<String, String> getTestRunArgMap() {
        return mArgMap;
    }

    /** @param runArgMap the arguments to pass to the test runner. */
    public void setTestRunArgMap(Map<String, String> runArgMap) {
        mArgMap = runArgMap;
    }
}