Merge "Android Open Accessory helper"
diff --git a/libraries/aupt-lib/Android.bp b/libraries/aupt-lib/Android.bp
index 3f8a718..35191f4 100644
--- a/libraries/aupt-lib/Android.bp
+++ b/libraries/aupt-lib/Android.bp
@@ -17,6 +17,7 @@
 java_library {
     name: "AuptLib",
     libs: [
+        "androidx.test.runner",
         "ub-uiautomator",
         "junit",
         "android.test.runner.stubs",
@@ -32,6 +33,7 @@
 
     platform_apis: true,
     static_libs: [
+        "androidx.test.runner",
         "ub-uiautomator",
         "junit",
     ],
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
index c5c986c..2400eea 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
@@ -31,6 +31,8 @@
 import android.test.InstrumentationTestRunner;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestCase;
@@ -114,6 +116,9 @@
     /* Test Initialization */
     @Override
     public void onCreate(Bundle params) {
+        // Support the newer AndroidX test initialization.
+        InstrumentationRegistry.registerInstance(this, params);
+
         mParams = params;
 
         // Parse out primitive parameters
diff --git a/libraries/collectors-helper/Android.bp b/libraries/collectors-helper/Android.bp
new file mode 100644
index 0000000..e6c1dc9
--- /dev/null
+++ b/libraries/collectors-helper/Android.bp
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 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.
+
+java_library {
+    name: "collectors-helper",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "platformprotoslite",
+        "statsdprotolite",
+        "androidx.test.runner",
+        "guava",
+    ],
+
+    platform_apis: true,
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java b/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java
new file mode 100644
index 0000000..b0a9ba0
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.AppStartOccurred;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AppStartupHelper consist of helper methods to set the app
+ * startup configs in statsd to track the app startup related
+ * performance metrics and retrieve the necessary information from
+ * statsd using the config id.
+ */
+public class AppStartupHelper implements ICollectorHelper<StringBuilder> {
+
+    private static final String LOG_TAG = AppStartupHelper.class.getSimpleName();
+    private static final String COLD_STARTUP = "cold_startup";
+    private static final String WARM_STARTUP = "warm_startup";
+    private static final String HOT_STARTUP = "hot_startup";
+
+    private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+    /**
+     * Set up the app startup statsd config to track the metrics during the app start occurred.
+     */
+    @Override
+    public boolean startCollecting() {
+        Log.i(LOG_TAG, "Adding AppStartOccured config to statsd.");
+        List<Integer> atomIdList = new ArrayList<>();
+        atomIdList.add(Atom.APP_START_OCCURRED_FIELD_NUMBER);
+        return mStatsdHelper.addEventConfig(atomIdList);
+    }
+
+    /**
+     * Collect the app startup metrics tracked during the app startup occurred from the statsd.
+     */
+    @Override
+    public Map<String, StringBuilder> getMetrics() {
+        List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+        Map<String, StringBuilder> appStartResultMap = new HashMap<>();
+        for (EventMetricData dataItem : eventMetricData) {
+            AppStartOccurred appStartAtom = dataItem.getAtom().getAppStartOccurred();
+            String pkgName = appStartAtom.getPkgName();
+            String transitionType = appStartAtom.getType().toString();
+            int windowsDrawnMillis = appStartAtom.getWindowsDrawnDelayMillis();
+            Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s,"
+                    + " WindowDrawnDelayMillis:%s", pkgName, transitionType, windowsDrawnMillis));
+
+            String metricKey = "";
+            switch (appStartAtom.getType()) {
+                case COLD:
+                    metricKey = MetricUtility.constructKey(COLD_STARTUP, appStartAtom.getPkgName());
+                    break;
+                case WARM:
+                    metricKey = MetricUtility.constructKey(WARM_STARTUP, appStartAtom.getPkgName());
+                    break;
+                case HOT:
+                    metricKey = MetricUtility.constructKey(HOT_STARTUP, appStartAtom.getPkgName());
+                    break;
+                case UNKNOWN:
+                    break;
+            }
+            if (!metricKey.isEmpty()) {
+                MetricUtility.addMetric(metricKey, windowsDrawnMillis, appStartResultMap);
+            }
+        }
+        return appStartResultMap;
+    }
+
+    /**
+     * Remove the statsd config used to track the app startup metrics.
+     */
+    @Override
+    public boolean stopCollecting() {
+        return mStatsdHelper.removeStatsConfig();
+    }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java b/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java
new file mode 100644
index 0000000..a2b3b3c
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.GaugeBucketInfo;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CpuUsageHelper consist of helper methods to set the app
+ * cpu usage configs in statsd to track the cpu usage related
+ * metrics and retrieve the necessary information from statsd
+ * using the config id.
+ */
+public class CpuUsageHelper implements ICollectorHelper<Long> {
+
+    private static final String LOG_TAG = CpuUsageHelper.class.getSimpleName();
+    private static final String CPU_USAGE_PKG_UID = "cpu_usage_pkg_or_uid";
+    private static final String CPU_USAGE_FREQ = "cpu_usage_freq";
+    private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
+    private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
+    private static final String CLUSTER_ID = "cluster";
+    private static final String FREQ_INDEX = "freq_index";
+    private static final String USER_TIME = "user_time";
+    private static final String SYSTEM_TIME = "system_time";
+
+    private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+
+    @Override
+    public boolean startCollecting() {
+        Log.i(LOG_TAG, "Adding CpuUsage config to statsd.");
+        List<Integer> atomIdList = new ArrayList<>();
+        // Add the atoms to be tracked.
+        atomIdList.add(Atom.CPU_TIME_PER_UID_FIELD_NUMBER);
+        atomIdList.add(Atom.CPU_TIME_PER_FREQ_FIELD_NUMBER);
+
+        return mStatsdHelper.addGaugeConfig(atomIdList);
+    }
+
+
+    @Override
+    public Map<String, Long> getMetrics() {
+        Map<String, Long> cpuUsageFinalMap = new HashMap<>();
+
+        List<GaugeMetricData> gaugeMetricList = mStatsdHelper.getGaugeMetrics();
+        ListMultimap<String, Long> cpuUsageMap = ArrayListMultimap.create();
+
+        for (GaugeMetricData gaugeMetric : gaugeMetricList) {
+            Log.v(LOG_TAG, "Bucket Size: " + gaugeMetric.getBucketInfoCount());
+            for (GaugeBucketInfo gaugeBucketInfo : gaugeMetric.getBucketInfoList()) {
+                for (Atom atom : gaugeBucketInfo.getAtomList()) {
+
+                    // Track CPU usage in user time and system time per package or UID
+                    if (atom.getCpuTimePerUid().hasUid()) {
+                        int uId = atom.getCpuTimePerUid().getUid();
+                        String packageName = mStatsdHelper.getPackageName(uId);
+                        long userTime = atom.getCpuTimePerUid().getUserTimeMillis();
+                        long sysTime = atom.getCpuTimePerUid().getSysTimeMillis();
+                        Log.v(LOG_TAG, String.format("Uid:%d, Pkg Name: %s, User_Time: %d,"
+                                + " System_Time: %d", uId, packageName, userTime, sysTime));
+
+                        // Use the package name if exist for the UID otherwise use the UID.
+                        // Note: UID for the apps will be different across the builds.
+
+                        // It is possible to have multiple bucket info. Track all the gauge info
+                        // and take the difference of the first and last to compute the
+                        // final usage.
+                        String finalUserTimeKey = MetricUtility.constructKey(CPU_USAGE_PKG_UID,
+                                (packageName == null) ? String.valueOf(uId) : packageName,
+                                USER_TIME);
+                        String finalSystemTimeKey = MetricUtility.constructKey(CPU_USAGE_PKG_UID,
+                                (packageName == null) ? String.valueOf(uId) : packageName,
+                                SYSTEM_TIME);
+                        cpuUsageMap.put(finalUserTimeKey, userTime);
+                        cpuUsageMap.put(finalSystemTimeKey, sysTime);
+                    }
+
+                    // Track cpu usage per cluster_id and freq_index
+                    if (atom.getCpuTimePerFreq().hasFreqIndex()) {
+                        int clusterId = atom.getCpuTimePerFreq().getCluster();
+                        int freqIndex = atom.getCpuTimePerFreq().getFreqIndex();
+                        long timeInFreq = atom.getCpuTimePerFreq().getTimeMillis();
+                        Log.v(LOG_TAG, String.format("Cluster Id: %d FreqIndex: %d,"
+                                + " Time_in_Freq: %d", clusterId, freqIndex, timeInFreq));
+                        String finalFreqIndexKey = MetricUtility.constructKey(
+                                CPU_USAGE_FREQ, CLUSTER_ID, String.valueOf(clusterId), FREQ_INDEX,
+                                String.valueOf(freqIndex));
+                        cpuUsageMap.put(finalFreqIndexKey, timeInFreq);
+                    }
+
+                }
+            }
+        }
+
+        // Compute the final result map
+        Long totalCpuUsage = 0L;
+        Long totalCpuFreq = 0L;
+        for (String key : cpuUsageMap.keySet()) {
+            List<Long> cpuUsageList = cpuUsageMap.get(key);
+            if (cpuUsageList.size() > 1) {
+                // Compute the total usage by taking the difference of last and first value.
+                Long cpuUsage = cpuUsageList.get(cpuUsageList.size() - 1)
+                        - cpuUsageList.get(0);
+                // Add the final result only if the cpu usage is greater than 0.
+                if (cpuUsage > 0) {
+                    cpuUsageFinalMap.put(key, cpuUsage);
+                }
+                // Add the CPU time to their respective (usage or frequency) total metric.
+                if (key.startsWith(CPU_USAGE_PKG_UID)) {
+                    totalCpuUsage += cpuUsage;
+                } else if (key.startsWith(CPU_USAGE_FREQ)) {
+                    totalCpuFreq += cpuUsage;
+                }
+            }
+        }
+        // Put the total results into the final result map.
+        cpuUsageFinalMap.put(TOTAL_CPU_USAGE, totalCpuUsage);
+        cpuUsageFinalMap.put(TOTAL_CPU_USAGE_FREQ, totalCpuFreq);
+        return cpuUsageFinalMap;
+    }
+
+    /**
+     * Remove the statsd config used to track the cpu usage metrics.
+     */
+    @Override
+    public boolean stopCollecting() {
+        return mStatsdHelper.removeStatsConfig();
+    }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java b/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java
new file mode 100644
index 0000000..495a442
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.ANROccurred;
+import com.android.os.AtomsProto.AppCrashOccurred;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CrashHelper consist of helper methods to set the crash collection
+ * configs in statsd to track the crashes happened during the test
+ * and retrieve the necessary information from statsd using the config id.
+ */
+public class CrashHelper implements ICollectorHelper<Integer> {
+
+    private static final String LOG_TAG = CrashHelper.class.getSimpleName();
+    private static final String TOTAL_PREFIX = "total_";
+    private static final String EVENT_JAVA_CRASH = "crash";
+    private static final String EVENT_NATIVE_CRASH = "native_crash";
+    private static final String EVENT_ANR = "anr";
+
+    private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+    /**
+     * Set up the app crash statsd config to track the crash metrics during the test.
+     */
+    @Override
+    public boolean startCollecting() {
+        Log.i(LOG_TAG, "Adding AppCrashOccured config to statsd.");
+        List<Integer> atomIdList = new ArrayList<>();
+        atomIdList.add(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER);
+        atomIdList.add(Atom.ANR_OCCURRED_FIELD_NUMBER);
+        return mStatsdHelper.addEventConfig(atomIdList);
+    }
+
+    /**
+     * Collect the app crash metrics tracked during the test from the statsd.
+     */
+    @Override
+    public Map<String, Integer> getMetrics() {
+        List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+        Map<String, Integer> appCrashResultMap = new HashMap<>();
+        // We need this because even if there are no crashes we need to report 0 count
+        // in the dashboard for the total crash, native crash and ANR.
+        appCrashResultMap.put(TOTAL_PREFIX + EVENT_JAVA_CRASH, 0);
+        appCrashResultMap.put(TOTAL_PREFIX + EVENT_NATIVE_CRASH, 0);
+        appCrashResultMap.put(TOTAL_PREFIX + EVENT_ANR, 0);
+        for (EventMetricData dataItem : eventMetricData) {
+            if (dataItem.getAtom().hasAppCrashOccurred()) {
+                AppCrashOccurred appCrashAtom = dataItem.getAtom().getAppCrashOccurred();
+                String eventType = appCrashAtom.getEventType();
+                String pkgName = appCrashAtom.getPackageName();
+                AppCrashOccurred.ForegroundState foregroundState =
+                        appCrashAtom.getForegroundState();
+                Log.i(LOG_TAG, String.format("Event Type:%s Pkg Name: %s "
+                        + " ForegroundState: %s", eventType, pkgName, foregroundState.toString()));
+
+                // Track the total crash and native crash count.
+                MetricUtility.addMetric(TOTAL_PREFIX + eventType, appCrashResultMap);
+                // Add more detailed crash count key metrics.
+                String detailKey = MetricUtility.constructKey(eventType, pkgName,
+                        foregroundState.toString());
+                MetricUtility.addMetric(detailKey, appCrashResultMap);
+            } else if (dataItem.getAtom().hasAnrOccurred()) {
+                ANROccurred anrAtom = dataItem.getAtom().getAnrOccurred();
+                String processName = anrAtom.getProcessName();
+                String reason = anrAtom.getReason();
+                ANROccurred.ForegroundState foregoundState = anrAtom.getForegroundState();
+                Log.i(LOG_TAG,
+                        String.format("ANR occurred in process %s due to %s; foregound state is %s",
+                                processName, reason, foregoundState.toString()));
+
+                // Track the total ANR count.
+                MetricUtility.addMetric(TOTAL_PREFIX + EVENT_ANR, appCrashResultMap);
+                String detailKey = MetricUtility.constructKey(
+                            EVENT_ANR, processName, reason, foregoundState.toString());
+                MetricUtility.addMetric(detailKey, appCrashResultMap);
+            }
+        }
+        return appCrashResultMap;
+    }
+
+    /**
+     * Remove the statsd config used to track the app crash metrics.
+     */
+    @Override
+    public boolean stopCollecting() {
+        return mStatsdHelper.removeStatsConfig();
+    }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java b/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java
new file mode 100644
index 0000000..c5b4922
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java
@@ -0,0 +1,22 @@
+package com.android.helpers;
+
+import java.util.Map;
+
+public interface ICollectorHelper<T> {
+
+    /**
+     * This method will have setup to start collecting the metrics.
+     */
+    boolean startCollecting();
+
+    /**
+     * This method will retrieve the metrics.
+     */
+    Map<String, T> getMetrics();
+
+    /**
+     * This method will do the tear down to stop collecting the metrics.
+     */
+    boolean stopCollecting();
+
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java b/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java
new file mode 100644
index 0000000..e251162
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java
@@ -0,0 +1,47 @@
+package com.android.helpers;
+
+import java.util.Map;
+
+/**
+ * MetricUtility consist of basic utility methods to construct the metrics
+ * reported at the end of the test.
+ */
+public class MetricUtility {
+
+    private static final String KEY_JOIN = "_";
+    private static final String METRIC_SEPARATOR = ",";
+
+    /**
+     * Append the given array of string to construct the final key used to track the metrics.
+     *
+     * @param keys to append using KEY_JOIN
+     */
+    public static String constructKey(String... keys) {
+        return String.join(KEY_JOIN, keys);
+    }
+
+    /**
+     * Add metric to the result map. If metric key already exist append the new metric.
+     *
+     * @param metricKey Unique key to track the metric.
+     * @param metric metric to track.
+     * @param resultMap map of all the metrics.
+     */
+    public static void addMetric(String metricKey, int metric, Map<String,
+            StringBuilder> resultMap) {
+        resultMap.compute(metricKey, (key, value) -> (value == null) ?
+                new StringBuilder().append(metric) : value.append(METRIC_SEPARATOR).append(metric));
+    }
+
+    /**
+     * Add metric to the result map. If metric key already exist increment the value by 1.
+     *
+     * @param metricKey Unique key to track the metric.
+     * @param resultMap map of all the metrics.
+     */
+    public static void addMetric(String metricKey, Map<String,
+            Integer> resultMap) {
+        resultMap.compute(metricKey, (key, value) -> (value == null) ? 1 : value + 1);
+    }
+
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java b/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java
new file mode 100644
index 0000000..1333eeb
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.content.Context;
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.StatsLog;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.FieldFilter;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.ConfigMetricsReport;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.StatsLog.StatsLogReport;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * StatsdHelper consist of basic utilities that will be used to setup statsd
+ * config, parse the collected information and remove the statsd config.
+ */
+public class StatsdHelper {
+    private static final String LOG_TAG = StatsdHelper.class.getSimpleName();
+    private static final long MAX_ATOMS = 2000;
+    private static final long METRIC_DELAY_MS = 3000;
+    private long mConfigId = -1;
+    private StatsManager mStatsManager;
+
+    /**
+     * Add simple event configurations using a list of atom ids.
+     *
+     * @param atomIdList uniquely identifies the information that we need to track by statsManager.
+     * @return true if the configuration is added successfully other wise false.
+     */
+    public boolean addEventConfig(List<Integer> atomIdList) {
+        long configId = System.currentTimeMillis();
+        StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
+
+        for (Integer atomId : atomIdList) {
+            int atomUniqueId = getUniqueId();
+            statsConfigBuilder
+                    .addEventMetric(
+                            EventMetric.newBuilder()
+                                    .setId(getUniqueId())
+                                    .setWhat(atomUniqueId))
+                    .addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId));
+        }
+        try {
+            getStatsManager().addConfig(configId, statsConfigBuilder.build().toByteArray());
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Not able to setup the event config.", e);
+            return false;
+        }
+        Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
+        setConfigId(configId);
+        return true;
+    }
+
+    /**
+     * Build gauge metric config based on trigger events (i.e AppBreadCrumbReported).
+     * Whenever the events are triggered via StatsLog.logEvent() collect the gauge metrics.
+     * It doesn't matter what the log event is. It could be 0 or 1.
+     * In order to capture the usage during the test take the difference of gauge metrics
+     * before and after the test.
+     *
+     * @param atomIdList List of atoms to be collected in gauge metrics.
+     * @return if the config is added successfully otherwise false.
+     */
+    public boolean addGaugeConfig(List<Integer> atomIdList) {
+        long configId = System.currentTimeMillis();
+        StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
+        int appBreadCrumbUniqueId = getUniqueId();
+
+        // Needed for collecting gauge metric based on trigger events.
+        statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(appBreadCrumbUniqueId,
+                Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
+
+        for (Integer atomId : atomIdList) {
+            int atomUniqueId = getUniqueId();
+            // Build Gauge metric config.
+            GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
+                    .setId(getUniqueId())
+                    .setWhat(atomUniqueId)
+                    .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
+                    .setMaxNumGaugeAtomsPerBucket(MAX_ATOMS)
+                    .setSamplingType(GaugeMetric.SamplingType.ALL_CONDITION_CHANGES)
+                    .setTriggerEvent(appBreadCrumbUniqueId)
+                    .setBucket(TimeUnit.CTS);
+
+            // add the gauge config.
+            statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId))
+                    .addGaugeMetric(gaugeMetric.build());
+        }
+
+        try {
+            getStatsManager().addConfig(configId,
+                    statsConfigBuilder.build().toByteArray());
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Not able to setup the gauge config.", e);
+            return false;
+        }
+
+        Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
+        setConfigId(configId);
+        return true;
+    }
+
+    /**
+     * Create simple atom matcher with the given id and the field id.
+     *
+     * @param id
+     * @param fieldId
+     * @return
+     */
+    private AtomMatcher.Builder getSimpleAtomMatcher(int id, int fieldId) {
+        return AtomMatcher.newBuilder()
+                .setId(id)
+                .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
+                        .setAtomId(fieldId));
+    }
+
+    /**
+     * List of authorized source that can write the information into statsd.
+     *
+     * @param configId unique id of the configuration tracked by StatsManager.
+     * @return
+     */
+    private static StatsdConfig.Builder getSimpleSources(long configId) {
+        return StatsdConfig.newBuilder().setId(configId)
+                .addAllowedLogSource("AID_ROOT")
+                .addAllowedLogSource("AID_SYSTEM")
+                .addAllowedLogSource("AID_RADIO")
+                .addAllowedLogSource("AID_BLUETOOTH")
+                .addAllowedLogSource("AID_GRAPHICS")
+                .addAllowedLogSource("AID_STATSD")
+                .addAllowedLogSource("AID_INCIENTD");
+    }
+
+    /**
+     * Returns the list of EventMetricData tracked under the config.
+     */
+    public List<EventMetricData> getEventMetrics() {
+        ConfigMetricsReportList reportList = null;
+        List<EventMetricData> eventData = new ArrayList<>();
+        try {
+            if (getConfigId() != -1) {
+                reportList = ConfigMetricsReportList.parser()
+                        .parseFrom(getStatsManager().getReports(getConfigId()));
+            }
+        } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+            Log.e(LOG_TAG, "Retreiving event metrics failed.", se);
+            return eventData;
+        }
+
+        if (reportList != null) {
+            ConfigMetricsReport configReport = reportList.getReports(0);
+            for (StatsLogReport metric : configReport.getMetricsList()) {
+                eventData.addAll(metric.getEventMetrics().getDataList());
+            }
+        }
+        Log.i(LOG_TAG, "Number of events: " + eventData.size());
+        return eventData;
+    }
+
+    /**
+     * Returns the list of GaugeMetric data tracked under the config.
+     */
+    public List<GaugeMetricData> getGaugeMetrics() {
+        // Dump the metric after the test is completed.
+        StatsLog.logEvent(0);
+        SystemClock.sleep(METRIC_DELAY_MS);
+        ConfigMetricsReportList reportList = null;
+        List<GaugeMetricData> gaugeData = new ArrayList<>();
+        try {
+            if (getConfigId() != -1) {
+                reportList = ConfigMetricsReportList.parser()
+                        .parseFrom(getStatsManager().getReports(getConfigId()));
+            }
+        } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+            Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se);
+            return gaugeData;
+        }
+
+        if (reportList != null) {
+            ConfigMetricsReport configReport = reportList.getReports(0);
+            for (StatsLogReport metric : configReport.getMetricsList()) {
+                gaugeData.addAll(metric.getGaugeMetrics().getDataList());
+            }
+        }
+        Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size());
+        return gaugeData;
+    }
+
+    /**
+     * Remove the existing config tracked in the statsd.
+     *
+     * @return true if the config is removed successfully otherwise false.
+     */
+    public boolean removeStatsConfig() {
+        Log.i(LOG_TAG, "Removing statsd config-id: " + getConfigId());
+        try {
+            getStatsManager().removeConfig(getConfigId());
+            Log.i(LOG_TAG, "Successfully removed config-id: " + getConfigId());
+            return true;
+        } catch (StatsUnavailableException e) {
+            Log.e(LOG_TAG, String.format("Not able to remove the config-id: %d due to %s ",
+                    getConfigId(), e.getMessage()));
+            return false;
+        }
+    }
+
+    /**
+     * StatsManager used to configure, collect and remove the statsd config.
+     *
+     * @return StatsManager
+     */
+    private StatsManager getStatsManager() {
+        if (mStatsManager == null) {
+            mStatsManager = (StatsManager) InstrumentationRegistry.getTargetContext().
+                    getSystemService(Context.STATS_MANAGER);
+        }
+        return mStatsManager;
+    }
+
+    /**
+     * Returns the package name for the UID if it is available. Otherwise return null.
+     *
+     * @param uid
+     * @return
+     */
+    public String getPackageName(int uid) {
+        String pkgName = InstrumentationRegistry.getTargetContext().getPackageManager()
+                .getNameForUid(uid);
+        // Remove the UID appended at the end of the package name.
+        if (pkgName != null) {
+            String pkgNameSplit[] = pkgName.split(String.format("\\:%d", uid));
+            return pkgNameSplit[0];
+        }
+        return pkgName;
+    }
+
+    /**
+     * Set the config id tracked in the statsd.
+     */
+    private void setConfigId(long configId) {
+        mConfigId = configId;
+    }
+
+    /**
+     * Return the config id tracked in the statsd.
+     */
+    private long getConfigId() {
+        return mConfigId;
+    }
+
+    /**
+     * Returns the unique id
+     */
+    private int getUniqueId() {
+        return UUID.randomUUID().hashCode();
+    }
+}
diff --git a/libraries/collectors-helper/test/Android.bp b/libraries/collectors-helper/test/Android.bp
new file mode 100644
index 0000000..116906a
--- /dev/null
+++ b/libraries/collectors-helper/test/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 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.
+
+java_library {
+    name: "collectors-helper-test",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.runner",
+        "android-support-test",
+        "collectors-helper",
+        "app-helpers-handheld-interfaces",
+        "junit",
+        "ub-uiautomator",
+    ],
+
+    platform_apis: true,
+}
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java
new file mode 100644
index 0000000..20605c1
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.ICalendarHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.AppStartupHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link AppStartupHelper}.
+ *
+ * To run:
+ * Connect to wifi and login to gmail.
+ * atest CollectorsHelperTest:com.android.helpers.tests.AppStartupHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppStartupHelperTest {
+
+    // Kill the calendar app.
+    private static final String KILL_TEST_APP_CMD_TEMPLATE = "am force-stop %s";
+    // Package names used for testing
+    private static final String CALENDAR_PKG_NAME = "com.google.android.calendar";
+    private static final String CALCULATOR_PKG_NAME = "com.google.android.calculator";
+    // Key prefixes to store the cold, warm or hot launch time of the calendar app, respectively.
+    private static final String COLD_LAUNCH_KEY_TEMPLATE = "cold_startup_%s";
+    private static final String WARM_LAUNCH_KEY_TEMPLATE = "warm_startup_%s";
+    private static final String HOT_LAUNCH_KEY_TEMPLATE = "hot_startup_%s";
+
+    // A couple of ADB commands to help with the calculator hot launch test.
+    private static final String LUANCH_APP_CMD_TEMPLATE =
+            "monkey -p %s -c android.intent.category.LAUNCHER 1";
+    private static final String KEYEVENT_CMD_TEMPLATE = "input keyevent %s";
+    private static final String KEYCODE_HOME = "KEYCODE_HOME";
+    private static final String KEYCODE_WAKEUP = "KEYCODE_WAKEUP";
+    private static final String KEYCODE_UNLOCK = "KEYCODE_MENU";
+
+    private AppStartupHelper mAppStartupHelper = new AppStartupHelper();
+    private HelperAccessor<ICalendarHelper> mHelper =
+            new HelperAccessor<>(ICalendarHelper.class);
+
+    @Before
+    public void setUp() {
+        mAppStartupHelper = new AppStartupHelper();
+        // Make sure the apps are starting from the clean state.
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
+    }
+
+    /**
+     * Test successfull app launch config.
+     */
+    @Test
+    public void testAppLaunchConfig() throws Exception {
+        assertTrue(mAppStartupHelper.startCollecting());
+        assertTrue(mAppStartupHelper.stopCollecting());
+    }
+
+    /**
+     * Test no error is thrown if there is no app launch.
+     */
+    @Test
+    public void testEmptyAppLaunchMetric() throws Exception {
+        assertTrue(mAppStartupHelper.startCollecting());
+        assertTrue(mAppStartupHelper.getMetrics().isEmpty());
+        assertTrue(mAppStartupHelper.stopCollecting());
+    }
+
+    /**
+     * Test cold launch key.
+     */
+    @Test
+    public void testColdLaunchMetricKey() throws Exception {
+        assertTrue(mAppStartupHelper.startCollecting());
+        mHelper.get().open();
+        Map.Entry<String, StringBuilder> appLaunchEntry = mAppStartupHelper.getMetrics().entrySet()
+                .iterator().next();
+        assertEquals(String.format(COLD_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME),
+                appLaunchEntry.getKey());
+        assertTrue(mAppStartupHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+    /**
+     * Test cold launch metric.
+     */
+    @Test
+    public void testSingleColdLaunchMetric() throws Exception {
+        assertTrue(mAppStartupHelper.startCollecting());
+        mHelper.get().open();
+        assertEquals(1, mAppStartupHelper.getMetrics().size());
+        assertTrue(mAppStartupHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+    /**
+     * Test multiple cold launch metric.
+     */
+    @Test
+    public void testMultipleColdLaunchMetric() throws Exception {
+        assertTrue(mAppStartupHelper.startCollecting());
+        mHelper.get().open();
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        mHelper.get().exit();
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+        mHelper.get().open();
+        Map.Entry<String, StringBuilder> appLaunchEntry = mAppStartupHelper.getMetrics().entrySet()
+                .iterator().next();
+        assertEquals(2, appLaunchEntry.getValue().toString().split(",").length);
+        assertTrue(mAppStartupHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+    /**
+     * Test warm launch metric.
+     */
+    @Test
+    public void testWarmLaunchMetric() throws Exception {
+
+        // Launch the app once and exit it so it resides in memory.
+        mHelper.get().open();
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        // Press home and clear the cache explicitly.
+        HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+        HelperTestUtility.clearCache();
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        // Start the collection here to test warm launch.
+        assertTrue(mAppStartupHelper.startCollecting());
+        // Launch the app; a warm launch occurs.
+        mHelper.get().open();
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
+        String calendarWarmLaunchKey = String.format(WARM_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(calendarWarmLaunchKey));
+        assertEquals(1, appLaunchMetrics.get(calendarWarmLaunchKey).toString().split(",").length);
+        assertTrue(mAppStartupHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+    /**
+     * Test hot launch metric on calculator, which is lightweight enough to trigger a hot launch.
+     */
+    @Test
+    public void testHotLaunchMetric() throws Exception {
+        String calculatorLaunchCommand =
+                String.format(LUANCH_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME);
+        // Launch the app once and go home so the app resides in memory.
+        HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_WAKEUP));
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_UNLOCK));
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        HelperTestUtility.executeShellCommand(calculatorLaunchCommand);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+        // Start the collection here to test hot launch.
+        assertTrue(mAppStartupHelper.startCollecting());
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        // Launch the app; a hot launch occurs.
+        HelperTestUtility.executeShellCommand(calculatorLaunchCommand);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
+        String calculatoHotLaunchKey = String.format(HOT_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(calculatoHotLaunchKey));
+        assertEquals(1, appLaunchMetrics.get(calculatoHotLaunchKey).toString().split(",").length);
+        assertTrue(mAppStartupHelper.stopCollecting());
+        HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
+    }
+}
+
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java
new file mode 100644
index 0000000..1cf74d3
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.ICalendarHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.CpuUsageHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link CpuUsageHelperTest}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.CpuUsageHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class CpuUsageHelperTest {
+
+    // Kill the calendar app.
+    private static final String KILL_TEST_APP_CMD = "am force-stop com.google.android.calendar";
+    // Key prefix used for cpu usage by frequency index.
+    private static final String CPU_USAGE_FREQ_PREFIX = "cpu_usage_freq";
+    // Key prefix used for cpu usage by package name or uid
+    private static final String CPU_USAGE_PKG_UID_PREFIX = "cpu_usage_pkg_or_uid";
+    // Key used for total CPU usage
+    private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
+    // Key used for total CPU usage in frequency buckets
+    private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
+
+    private CpuUsageHelper mCpuUsageHelper = new CpuUsageHelper();
+    private HelperAccessor<ICalendarHelper> mHelper =
+            new HelperAccessor<>(ICalendarHelper.class);
+
+    @Before
+    public void setUp() {
+        mCpuUsageHelper = new CpuUsageHelper();
+    }
+
+    /**
+     * Test successfull cpu usage config.
+     */
+    @Test
+    public void testCpuUsageConfig() throws Exception {
+        assertTrue(mCpuUsageHelper.startCollecting());
+        assertTrue(mCpuUsageHelper.stopCollecting());
+    }
+
+    /**
+     * Test cpu usage metrics are collected.
+     */
+    @Test
+    public void testCpuUsageMetrics() throws Exception {
+        assertTrue(mCpuUsageHelper.startCollecting());
+        mHelper.get().open();
+        Map<String, Long> cpuUsage = mCpuUsageHelper.getMetrics();
+        assertTrue(cpuUsage.size() > 0);
+        assertTrue(mCpuUsageHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+    /**
+     * Test that at least one cpu usage per pkg or uid and per preq index is collected,
+     * the total usage is collected, and that
+     * the total usage is indeed the sum of the per pkg/uid and frequency usage, respectively.
+     */
+    @Test
+    public void testCpuUsageMetricsKey() throws Exception {
+        // Variables to verify existence of collected metrics.
+        boolean isFreqIndexPresent = false;
+        boolean isPkgorUidPresent = false;
+        boolean isFreqUsed = false;
+        boolean isUIDUsed = false;
+        boolean isTotalCpuUsageEntryPresent = false;
+        boolean isTotalCpuUsageValuePresent = false;
+        boolean isTotalCpuFreqEntryPresent = false;
+        boolean isTotalCpuFreqValuePresent = false;
+        assertTrue(mCpuUsageHelper.startCollecting());
+        mHelper.get().open();
+        // Variables to Verify that the reported usage does sum up to the reported total usage.
+        Long sumCpuUsage = 0L;
+        Long sumCpuUsageFreq = 0L;
+        Long reportedTotalCpuUsage = 0L;
+        Long reportedTotalCpuUsageFreq = 0L;
+        for (Map.Entry<String, Long> cpuUsageEntry : mCpuUsageHelper.getMetrics().entrySet()) {
+            if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_FREQ_PREFIX)) {
+                isFreqIndexPresent = true;
+                if (cpuUsageEntry.getValue() > 0) {
+                    isFreqUsed = true;
+                }
+                sumCpuUsageFreq += cpuUsageEntry.getValue();
+            }
+            if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_PKG_UID_PREFIX)) {
+                isPkgorUidPresent = true;
+                if (cpuUsageEntry.getValue() > 0) {
+                    isUIDUsed = true;
+                }
+                sumCpuUsage += cpuUsageEntry.getValue();
+            }
+            if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE_FREQ)) {
+                isTotalCpuFreqEntryPresent = true;
+                if (cpuUsageEntry.getValue() > 0) {
+                    isTotalCpuFreqValuePresent = true;
+                }
+                reportedTotalCpuUsageFreq = cpuUsageEntry.getValue();
+            }
+            if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE)) {
+                isTotalCpuUsageEntryPresent = true;
+                if (cpuUsageEntry.getValue() > 0) {
+                    isTotalCpuUsageValuePresent = true;
+                }
+                reportedTotalCpuUsage = cpuUsageEntry.getValue();
+            }
+        }
+        assertTrue(isFreqIndexPresent && isFreqUsed);
+        assertTrue(isPkgorUidPresent && isUIDUsed);
+        assertTrue(isTotalCpuUsageEntryPresent && isTotalCpuUsageValuePresent);
+        assertTrue(isTotalCpuFreqEntryPresent && isTotalCpuFreqValuePresent);
+        assertEquals(sumCpuUsageFreq, reportedTotalCpuUsageFreq);
+        assertEquals(sumCpuUsage, reportedTotalCpuUsage);
+        assertTrue(mCpuUsageHelper.stopCollecting());
+        mHelper.get().exit();
+    }
+
+}
+
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java
new file mode 100644
index 0000000..125490f
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.By;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.CrashHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link CrashHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.CrashHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class CrashHelperTest {
+
+    // Launch Development app BadBehavior activity.
+    private static final String START_APP =
+            "am start -W -n com.android.development/.BadBehaviorActivity";
+    // Kill the Development app
+    private static final String KILL_TEST_APP_CMD = "am force-stop com.android.development";
+    // Package name of the development app.
+    private static final String PKG_NAME = "com.android.development";
+    // Key used to store the crash count.
+    private static final String TOTAL_CRASHES_KEY = "total_crash";
+    // Key used to store the native crash.
+    private static final String TOTAL_NATIVE_CRASHES_KEY = "total_native_crash";
+    // Key used to store the ANR.
+    private static final String TOTAL_ANRS_KEY = "total_anr";
+    // Detailed crash key associated with the package name and the foreground status.
+    private static final String CRASH_PKG_KEY = "crash_com.android.development_FOREGROUND";
+    // Detailed native crash key associated with the package name and the foreground status.
+    private static final String NATIVE_CRASH_PKG_KEY =
+            "native_crash_com.android.development_FOREGROUND";
+    // Detailed event key associated with the ANR: process, reason and foreground status.
+    private static final String ANR_DETAIL_KEY = "anr_com.android.development"
+            + "_executing service com.android.development/.BadBehaviorActivity$BadService"
+            + "_FOREGROUND";
+    // Button id to cause the crash.
+    private static final String CRASH_BTN_NAME = "bad_behavior_crash_main";
+    // Button id to cause the native crash.
+    private static final String NATIVE_CRASH_BTN_NAME = "bad_behavior_crash_native";
+    // Button id to cause the an ANR (not all ANR-related buttons work, but this one does).
+    private static final String ANR_SERVICE_BTN_NAME = "bad_behavior_anr_service";
+    // This delay ensures that an ANR is actually logged.
+    // For details, see BadBehaviorActivity.BadService.
+    private static final int ANR_DELAY = 40000;
+
+    private CrashHelper mCrashHelper = new CrashHelper();
+
+    @Before
+    public void setUp() {
+        mCrashHelper = new CrashHelper();
+        // Make the apps are starting from the clean state.
+        HelperTestUtility.clearApp(KILL_TEST_APP_CMD);
+    }
+
+    /**
+     * Test successfull crash config.
+     */
+    @Test
+    public void testCrashConfig() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+        assertTrue(mCrashHelper.stopCollecting());
+    }
+
+    /**
+     * Test no crash metrics.
+     */
+    @Test
+    public void testEmptyCrashMetric() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+        Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+        // "crash", "native_crash" and "anr" keys with value 0
+        assertEquals(3, crashMap.size());
+        assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+        assertTrue(mCrashHelper.stopCollecting());
+
+    }
+
+    /**
+     * Test one crash metric.
+     */
+    @Test
+    public void testCrashMetric() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+        HelperTestUtility.executeShellCommand(START_APP);
+        invokeBehavior(CRASH_BTN_NAME);
+        Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+        // An empty ANR key in addition to the crash keys.
+        assertEquals(4, crashMap.size());
+        assertEquals(1, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+        assertEquals(1, crashMap.get(CRASH_PKG_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+        assertTrue(mCrashHelper.stopCollecting());
+
+    }
+
+    /**
+     * Test native crash metric.
+     */
+    @Test
+    public void testNativeCrashMetric() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+        HelperTestUtility.executeShellCommand(START_APP);
+        invokeBehavior(NATIVE_CRASH_BTN_NAME);
+        Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+        // An empty ANR key in addition to the crash keys.
+        assertEquals(4, crashMap.size());
+        assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+        assertEquals(1, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+        assertEquals(1, crashMap.get(NATIVE_CRASH_PKG_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+        assertTrue(mCrashHelper.stopCollecting());
+
+    }
+
+    /**
+     * Test ANR metric.
+     */
+    @Test
+    public void testAnrMetric() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+        HelperTestUtility.executeShellCommand(START_APP);
+        // The 30100ms sleep time guarantees that an ANR is indeed triggered.
+        // See BadBehaviorActivity.BadService for details.
+        invokeBehavior(ANR_SERVICE_BTN_NAME, ANR_DELAY);
+        Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+        // Two ANR keys and two empty crash keys.
+        assertEquals(4, crashMap.size());
+        assertEquals(1, crashMap.get(TOTAL_ANRS_KEY).intValue());
+        assertEquals(1, crashMap.get(ANR_DETAIL_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+        assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+        assertTrue(mCrashHelper.stopCollecting());
+    }
+
+    /**
+     * Test both crash and native crash.
+     */
+    @Test
+    public void testMultipleCrashMetric() throws Exception {
+        assertTrue(mCrashHelper.startCollecting());
+
+        // Invoke the crash
+        HelperTestUtility.executeShellCommand(START_APP);
+        invokeBehavior(CRASH_BTN_NAME);
+
+        // Invoke the native crash
+        HelperTestUtility.executeShellCommand(START_APP);
+        invokeBehavior(NATIVE_CRASH_BTN_NAME);
+
+        // Invoke the ANR.
+        HelperTestUtility.executeShellCommand(START_APP);
+        invokeBehavior(ANR_SERVICE_BTN_NAME, ANR_DELAY);
+
+        Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+        // Two keys for each crash metric, totaling 6.
+        assertEquals(6, crashMap.size());
+        assertEquals(1, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+        assertEquals(1, crashMap.get(CRASH_PKG_KEY).intValue());
+        assertEquals(1, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+        assertEquals(1, crashMap.get(NATIVE_CRASH_PKG_KEY).intValue());
+        assertEquals(1, crashMap.get(TOTAL_ANRS_KEY).intValue());
+        assertEquals(1, crashMap.get(ANR_DETAIL_KEY).intValue());
+        assertTrue(mCrashHelper.stopCollecting());
+    }
+
+    /**
+     * Cause the behavior by clicking on the button in bad behaviour activity.
+     */
+    private void invokeBehavior(String resourceName, int delayAfterAction) {
+        UiObject2 behaviorButton = HelperTestUtility.getUiDevice().findObject(
+                By.res(PKG_NAME, resourceName));
+        behaviorButton.click();
+        // Some actions, e.g. service ANR, requires a delay to register.
+        SystemClock.sleep(delayAfterAction);
+
+        // Dismiss the crash dialog which sometimes appear.
+        UiObject2 closeButton = HelperTestUtility.getUiDevice().findObject(
+                By.res("android", "aerr_close"));
+        if (closeButton != null) {
+            closeButton.click();
+        }
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+    }
+
+    /**
+     * Convenience function for "immediate" actions such as crashes.
+     */
+    private void invokeBehavior(String resourceName) {
+        invokeBehavior(resourceName, 0);
+    }
+}
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java
new file mode 100644
index 0000000..54f94d1
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+
+public class HelperTestUtility {
+
+    // Clear the cache
+    private static final String CLEAR_CACHE = "echo 3 > /proc/sys/vm/drop_caches";
+    // Delay between actions happening in the device.
+    public static final int ACTION_DELAY = 2000;
+
+    private static UiDevice mDevice;
+
+    /**
+     * Returns the active {@link UiDevice} to interact with.
+     */
+    public static UiDevice getUiDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        }
+        return mDevice;
+    }
+
+    /**
+     * Runs a shell command, {@code cmd}, and returns the output.
+     */
+    public static String executeShellCommand(String cmd) {
+        try {
+            return getUiDevice().executeShellCommand(cmd);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Clears the cache.
+     */
+    public static void clearCache() {
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        executeShellCommand(CLEAR_CACHE);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+    }
+
+    /**
+     * Close the test app and clear the cache.
+     * TODO: Replace it with the rules.
+     */
+    public static void clearApp(String killCmd) {
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        executeShellCommand(killCmd);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        executeShellCommand(CLEAR_CACHE);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+    }
+
+}
+
diff --git a/libraries/composer/host/Android.bp b/libraries/composer/host/Android.bp
index d8e4f10..aa12779 100644
--- a/libraries/composer/host/Android.bp
+++ b/libraries/composer/host/Android.bp
@@ -12,9 +12,47 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+java_genrule {
+    name: "profile-text-protos",
+    host_supported: true,
+    tools: ["aprotoc"],
+    srcs: [
+        "src/**/profile.proto",
+        "res/*.textpb",
+    ],
+    out: ["profiles.jar"],
+    /*
+     * Loops over all *.textpb files under res/ and serializes them into binary protos with ".pb"
+     * extension using aprotoc. The generated *.pb files are put under an assets/ directory, which
+     * gets packed into a .jar file and ends being under the assets/ directory in the apk package,
+     * which can then be read by the app's asset manager.
+     *
+     * If a profile fails to parse, an error is thrown and the build will fail.
+     */
+    cmd: "out_dir=$$(dirname $(out)) && assets_dir=\"assets\" && mkdir -p $$out_dir/$$assets_dir " +
+        "&& src_protos=($(locations res/*.textpb)) && red='\033[0;31m' && no_color='\033[0m' " +
+        "&& for file in $${src_protos[@]} ; do fname=$$(basename $$file) " +
+        "&& if ! ($(location aprotoc) --encode=composer.profile.Configuration " +
+        "$(location src/**/profile.proto) < $$file > " +
+        "$$out_dir/$$assets_dir/$${fname//.textpb/.pb}) ; then " +
+        "echo \"$${red}Failed to parse profile $$file. See above for errors.$${no_color}\" " +
+        "&& exit 1 ; fi ; done && jar cf $(out) -C $$(dirname $(out)) $$assets_dir",
+}
+
 java_library_static {
     name: "test-composers",
-    srcs: [ "src/**/*.java" ],
+    static_libs: [
+        "junit",
+        "profile-text-protos",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.proto",
+    ],
+    proto: {
+        type: "lite",
+        include_dirs: ["external/protobuf/src"],
+    },
     sdk_version: "current",
     host_supported: true,
 }
diff --git a/libraries/composer/host/res/sample_profile.textpb b/libraries/composer/host/res/sample_profile.textpb
new file mode 100644
index 0000000..a80d08f
--- /dev/null
+++ b/libraries/composer/host/res/sample_profile.textpb
@@ -0,0 +1,19 @@
+scheduled {
+    if_early: IDLE
+}
+scenarios [{
+    at: "00:00:10"
+    journey: "android.platform.test.scenario.calendar.FlingDayPage"
+}, {
+    at: "00:01:00"
+    journey: "android.platform.test.scenario.calendar.FlingWeekPage"
+}, {
+    at: "00:02:00"
+    journey: "android.platform.test.scenario.gmail.OpenApp"
+}, {
+    at: "00:03:00"
+    journey: "android.platform.test.scenario.quicksettings.ToggleWifiOff"
+}, {
+    at: "00:04:00"
+    journey: "android.platform.test.scenario.quicksettings.ToggleWifiOn"
+}]
diff --git a/libraries/composer/host/src/android/host/test/composer/Profile.java b/libraries/composer/host/src/android/host/test/composer/Profile.java
new file mode 100644
index 0000000..51ff7b9
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/Profile.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+import android.host.test.composer.profile.Configuration.Scheduled;
+import android.host.test.composer.profile.Configuration.Scheduled.IfEarly;
+import android.host.test.composer.profile.Configuration.Scheduled.IfLate;
+
+import java.util.Map;
+
+/**
+ * An extension of {@link android.host.test.composer.ProfileBase} for host-side testing.
+ */
+public class Profile extends ProfileBase<Map<String, String>> {
+    @Override
+    protected Configuration getConfigurationArgument(Map<String, String> args) {
+        if (!args.containsKey(PROFILE_OPTION_NAME)) {
+            return null;
+        }
+        return getStubConfiguration();
+    }
+
+    private Configuration getStubConfiguration() {
+        return Configuration.newBuilder()
+            .setScheduled(
+                    Scheduled.newBuilder()
+                        .setIfEarly(IfEarly.SLEEP)
+                        .setIfLate(IfLate.END))
+            .addScenarios(
+                    Scenario.newBuilder()
+                        .setWeight(0.5)
+                        .setJourney("android.platform.test.scenario.calendar.FlingWeekPage")
+                )
+            .addScenarios(
+                    Scenario.newBuilder()
+                        .setWeight(0.25)
+                        .setJourney("android.platform.test.scenario.calendar.FlingDayPage")
+                )
+            .addScenarios(
+                    Scenario.newBuilder()
+                        .setWeight(0.25)
+                        .setJourney("android.platform.test.scenario.calendar.FlingWeekPage")
+                )
+            .build();
+    }
+}
diff --git a/libraries/composer/host/src/android/host/test/composer/ProfileBase.java b/libraries/composer/host/src/android/host/test/composer/ProfileBase.java
new file mode 100644
index 0000000..2424b9a
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/ProfileBase.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+
+import java.lang.IllegalArgumentException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * A {@link Compose} function base class for taking a profile and returning the tests in the specified sequence.
+ */
+public abstract class ProfileBase<T> implements Compose<T, Runner> {
+    protected static final String PROFILE_OPTION_NAME = "profile";
+    protected static final String PROFILE_EXTENSION = ".pb";
+
+    private static final String LOG_TAG = ProfileBase.class.getSimpleName();
+
+    // Store the configuration to be read by the test runner.
+    private Configuration mConfiguration;
+
+    // Comparator for sorting timstamped CUJs.
+    private static class ScenarioTimestampComparator implements Comparator<Scenario> {
+        public int compare(Scenario s1, Scenario s2) {
+            if (! (s1.hasAt() && s2.hasAt())) {
+                throw new IllegalArgumentException(
+                      "Scenarios in scheduled profiles must have timestamps.");
+            }
+            return s1.getAt().compareTo(s2.getAt());
+        }
+    }
+
+    @Override
+    public List<Runner> apply(T args, List<Runner> input) {
+        mConfiguration = getConfigurationArgument(args);
+        if (mConfiguration == null) {
+            return input;
+        }
+        return getTestSequenceFromConfiguration(mConfiguration, input);
+    }
+
+    public boolean isConfigurationLoaded() {
+        return (mConfiguration == null);
+    }
+
+    public Configuration getLoadedConfiguration() {
+        return mConfiguration;
+    }
+
+    protected List<Runner> getTestSequenceFromConfiguration(
+            Configuration config, List<Runner> input) {
+        Map<String, Runner> nameToRunner =
+                input.stream().collect(
+                        Collectors.toMap(
+                                r -> r.getDescription().getDisplayName(), Function.identity()));
+        logInfo(LOG_TAG, String.format(
+                "Available scenarios: %s",
+                nameToRunner.keySet().stream().collect(Collectors.joining(", "))));
+        List<Scenario> scenarios = new ArrayList<Scenario>(config.getScenariosList());
+        if (config.hasScheduled()) {
+            Collections.sort(scenarios, new ScenarioTimestampComparator());
+        }
+        List<Runner> result = scenarios
+                .stream()
+                .map(Configuration.Scenario::getJourney)
+                .map(
+                        journeyName -> {
+                            if (nameToRunner.containsKey(journeyName)) {
+                                return nameToRunner.get(journeyName);
+                            } else {
+                                throw new IllegalArgumentException(
+                                        String.format(
+                                                "Journey %s in profile does not exist.",
+                                                journeyName));
+                            }
+                        })
+                .collect(Collectors.toList());
+        logInfo(LOG_TAG, String.format(
+                "Returned runners: %s",
+                result.stream()
+                            .map(Runner::getDescription)
+                            .map(Description::getDisplayName)
+                            .collect(Collectors.toList())));
+        return result;
+    }
+
+    /**
+     * Parses the arguments, reads the configuration file and returns the Configuraiton object.
+     *
+     * If no profile option is found in the arguments, function should return null, in which case
+     * the input sequence is returned without modification. Otherwise, function should parse the
+     * profile according to the supplied argument and return the Configuration object or throw an
+     * exception if the file is not available or cannot be parsed.
+     */
+    protected abstract Configuration getConfigurationArgument(T args);
+
+    /**
+     * Overrideable, platform-dependent logging function.
+     */
+    protected void logInfo(String tag, String content) {
+        System.out.println(content);
+    }
+}
diff --git a/libraries/composer/host/src/android/host/test/composer/profile.proto b/libraries/composer/host/src/android/host/test/composer/profile.proto
new file mode 100644
index 0000000..c339b2f
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/profile.proto
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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.
+
+syntax = "proto2";
+
+package composer.profile;
+
+option java_package = "android.host.test.composer.profile";
+option java_multiple_files = true;
+
+message Configuration {
+    // Types required to build a profile.
+
+    //
+    message Scheduled {
+        //
+        enum IfEarly {
+            IDLE = 1;
+            SLEEP = 2;
+        }
+        //
+        enum IfLate {
+            DELAY = 1;
+            END = 2;
+        }
+        optional IfEarly if_early = 1;
+        optional IfLate if_late = 2;
+    }
+
+    //
+    message Weighted { }
+
+    //
+    message Scenario {
+        oneof schedule {
+            //
+            string at = 1;
+            //
+            double weight = 2;
+        }
+        //
+        optional string journey = 3;
+        //
+        message ExtraArg {
+            optional string key = 1;
+            optional string value = 2;
+        }
+        //
+        repeated ExtraArg extras = 4;
+        //map<string, string> extras = 4;
+    }
+
+    // Field definitions for a profile.
+
+    oneof schedule {
+        Scheduled scheduled = 1;
+        Weighted weighted = 2;
+    }
+    repeated Scenario scenarios = 3;
+}
diff --git a/libraries/composer/host/tests/Android.bp b/libraries/composer/host/tests/Android.bp
index 92d9554..ae35980 100644
--- a/libraries/composer/host/tests/Android.bp
+++ b/libraries/composer/host/tests/Android.bp
@@ -22,6 +22,17 @@
         "test-composers",
         "truth-prebuilt",
     ],
+    target: {
+        android: {
+            static_libs: ["mockito-target"]
+        },
+        host: {
+            static_libs: ["mockito", "objenesis"]
+        }
+    },
+    static_libs: [
+        "guava",
+    ]
 }
 
 //#####################################
diff --git a/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java
new file mode 100644
index 0000000..764218c
--- /dev/null
+++ b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.runner.Runner;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit test the logic for host-side {@link Profile}
+ */
+@RunWith(JUnit4.class)
+public class ProfileTest extends ProfileTestBase<Map<String, String>> {
+    protected class TestableProfile extends ProfileBase<Map<String, String>> {
+        @Override
+        protected Configuration getConfigurationArgument(Map<String, String> args) {
+            return TEST_CONFIGS.get(args.get(PROFILE_OPTION_NAME));
+        }
+    }
+
+    @Override
+    protected ProfileBase<Map<String, String>> getProfile() {
+        return new TestableProfile();
+    }
+
+    @Override
+    protected Map<String, String> getArguments(String configName) {
+        HashMap<String, String> args = new HashMap<>();
+        args.put(PROFILE_OPTION_NAME, configName);
+        return args;
+    }
+}
diff --git a/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java
new file mode 100644
index 0000000..10ed8cf
--- /dev/null
+++ b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+import android.host.test.composer.profile.Configuration.Scheduled;
+import android.host.test.composer.profile.Configuration.Scheduled.IfEarly;
+import android.host.test.composer.profile.Configuration.Scheduled.IfLate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Base class to unit test the logic for {@link Profile}
+ */
+public abstract class ProfileTestBase<T> {
+    protected static final String PROFILE_OPTION_NAME = "profile";
+
+    protected static final String VALID_CONFIG_KEY = "valid_config";
+    protected static final Configuration VALID_CONFIG = Configuration.newBuilder()
+            .setScheduled(
+                    Scheduled.newBuilder()
+                            .setIfEarly(IfEarly.SLEEP)
+                            .setIfLate(IfLate.END))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:01:00")
+                            .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:04:00")
+                            .setJourney("android.platform.test.scenario.calendar.FlingDayPage"))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:02:00")
+                            .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+            .build();
+    private static final String CONFIG_WITH_INVALID_JOURNEY_KEY = "config_with_invalid_journey";
+    protected static final Configuration CONFIG_WITH_INVALID_JOURNEY = Configuration.newBuilder()
+            .setScheduled(
+                    Scheduled.newBuilder()
+                            .setIfEarly(IfEarly.SLEEP)
+                            .setIfLate(IfLate.END))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:01:00")
+                            .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:02:00")
+                            .setJourney("invalid"))
+            .build();
+    private static final String CONFIG_WITH_MISSING_TIMESTAMPS_KEY =
+            "config_with_missing_timestamps";
+    protected static final Configuration CONFIG_WITH_MISSING_TIMESTAMPS = Configuration
+            .newBuilder()
+            .setScheduled(
+                    Scheduled.newBuilder()
+                            .setIfEarly(IfEarly.SLEEP)
+                            .setIfLate(IfLate.END))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setAt("00:01:00")
+                            .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+            .addScenarios(
+                    Scenario.newBuilder()
+                            .setWeight(0.1)
+                            .setJourney("android.platform.test.scenario.calendar.FlingDayPage"))
+            .build();
+    protected static final ImmutableMap<String, Configuration> TEST_CONFIGS= ImmutableMap.of(
+            VALID_CONFIG_KEY, VALID_CONFIG,
+            CONFIG_WITH_INVALID_JOURNEY_KEY, CONFIG_WITH_INVALID_JOURNEY,
+            CONFIG_WITH_MISSING_TIMESTAMPS_KEY, CONFIG_WITH_MISSING_TIMESTAMPS);
+    private static final ImmutableList<String> AVAILABLE_JOURNEYS = ImmutableList.of(
+            "android.platform.test.scenario.calendar.FlingWeekPage",
+            "android.platform.test.scenario.calendar.FlingDayPage",
+            "android.platform.test.scenario.calendar.FlingSchedulePage");
+
+    private ArrayList<Runner> mMockInput;
+
+    @Rule
+    public ExpectedException exceptionThrown = ExpectedException.none();
+
+    /**
+     * Sets up the input list of mocked runners for test.
+     */
+    @Before
+    public void setUp() {
+        mMockInput = new ArrayList<Runner>();
+        for (String testJourney : AVAILABLE_JOURNEYS) {
+            Runner mockRunner = Mockito.mock(Runner.class);
+            Description mockDescription = Mockito.mock(Description.class);
+            Mockito.when(mockDescription.getDisplayName()).thenReturn(testJourney);
+            Mockito.when(mockRunner.getDescription()).thenReturn(mockDescription);
+            mMockInput.add(mockRunner);
+        }
+    }
+
+    /**
+     * Tests that the returned runners are ordered according to their scheduled timestamps.
+     */
+    @Test
+    public void testProfileOrderingRespected() {
+        ImmutableList<String> expectedJourneyOrder = ImmutableList.of(
+            "android.platform.test.scenario.calendar.FlingWeekPage",
+            "android.platform.test.scenario.calendar.FlingWeekPage",
+            "android.platform.test.scenario.calendar.FlingDayPage");
+
+        List<Runner> output = getProfile().apply(getArguments(VALID_CONFIG_KEY), mMockInput);
+        List<String> outputDescriptions = output.stream().map(r ->
+                r.getDescription().getDisplayName()).collect(Collectors.toList());
+        boolean respected = outputDescriptions.equals(expectedJourneyOrder);
+        System.out.println(outputDescriptions);
+        assertThat(respected).isTrue();
+    }
+
+    /**
+     * Tests that an exception is thrown for profiles with invalid scenario names.
+     */
+    @Test
+    public void testProfileWithInvalidScenarioThrows() {
+        // An exception about nonexistent user journey should be thrown.
+        exceptionThrown.expect(IllegalArgumentException.class);
+        exceptionThrown.expectMessage("does not exist");
+        exceptionThrown.expectMessage("invalid");
+
+        // Attempt to apply a profile with invalid CUJ; the above exception should be thrown.
+        List<Runner> output = getProfile().apply(
+                getArguments(CONFIG_WITH_INVALID_JOURNEY_KEY), mMockInput);
+    }
+
+    /**
+     * Tests that an exception is thrown for profiles with mixed weighted/timestamped scenarios."
+     */
+    @Test
+    public void testScheduledScenarioWithoutTimestampThrows() {
+        // An exception about the absence of a timestamp should be thrown.
+        exceptionThrown.expect(IllegalArgumentException.class);
+        exceptionThrown.expectMessage("must have timestamps");
+        exceptionThrown.expectMessage("scheduled");
+
+        // Attempt to apply a scheduled profile with missing timestamps; the above exception should
+        // be thrown.
+        List<Runner> output = getProfile().apply(
+                getArguments(CONFIG_WITH_MISSING_TIMESTAMPS_KEY), mMockInput);
+    }
+
+    protected abstract ProfileBase<T> getProfile();
+
+    protected abstract T getArguments(String configName);
+}
diff --git a/libraries/composer/platform/Android.bp b/libraries/composer/platform/Android.bp
index 8c2a597..268caba 100644
--- a/libraries/composer/platform/Android.bp
+++ b/libraries/composer/platform/Android.bp
@@ -17,5 +17,5 @@
     srcs: [ "src/**/*.java" ],
     sdk_version: "current",
     libs: [ "androidx.test.runner" ],
-    static_libs: ["test-composers"],
+    static_libs: ["test-composers", "guava"],
 }
diff --git a/libraries/composer/platform/src/android/platform/test/composer/Profile.java b/libraries/composer/platform/src/android/platform/test/composer/Profile.java
new file mode 100644
index 0000000..b612714
--- /dev/null
+++ b/libraries/composer/platform/src/android/platform/test/composer/Profile.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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 android.platform.test.composer;
+
+import android.content.res.AssetManager;
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.ProfileBase;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.IllegalArgumentException;
+
+/**
+ * An extension of {@link android.host.test.composer.ProfileBase} for device-side testing.
+ */
+public class Profile extends ProfileBase<Bundle> {
+    /*
+     * {@inheritDocs}
+     *
+     * The configuration should be passed as either the name of a configuration bundled into the APK
+     * or a path to the configuration file.
+     *
+     * TODO(harrytczhang@): Write tests for this logic.
+     */
+    @Override
+    protected Configuration getConfigurationArgument(Bundle args) {
+        // profileValue is either the name of a profile bundled with an APK or a path to a
+        // profile configuration file.
+        String profileValue = args.getString(PROFILE_OPTION_NAME, "");
+        if (profileValue.isEmpty()) {
+            return null;
+        }
+        // Look inside the APK assets for the profile; if this fails, try
+        // using the profile argument as a path to a configuration file.
+        InputStream configStream;
+        try {
+            AssetManager manager = InstrumentationRegistry.getContext().getAssets();
+            String profileName = profileValue + PROFILE_EXTENSION;
+            configStream = manager.open(profileName);
+        } catch (IOException e) {
+            // Try using the profile argument it as a path to a configuration file.
+            try {
+                File configFile = new File(profileValue);
+                if (!configFile.exists()) {
+                    throw new IllegalArgumentException(String.format(
+                            "Profile %s does not exist.", profileValue));
+                }
+                configStream = new FileInputStream(configFile);
+            } catch (IOException f) {
+                throw new IllegalArgumentException(String.format(
+                        "Profile %s cannot be opened.", profileValue));
+            }
+        }
+        try {
+            // Parse the configuration from its input stream and return it.
+            return Configuration.parseFrom(configStream);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(String.format(
+                    "Cannot parse profile %s.", profileValue));
+        } finally {
+            try {
+                configStream.close();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    @Override
+    protected void logInfo(String tag, String content) {
+        Log.i(tag, content);
+    }
+}
diff --git a/libraries/composer/platform/tests/Android.bp b/libraries/composer/platform/tests/Android.bp
index 01b6e07..a3ca3d6 100644
--- a/libraries/composer/platform/tests/Android.bp
+++ b/libraries/composer/platform/tests/Android.bp
@@ -23,6 +23,7 @@
         "platform-test-composers",
         "test-composers-tests",
         "truth-prebuilt",
+        "guava",
     ],
     srcs: [ "src/**/*.java" ],
     test_suites: [ "device-tests" ],
diff --git a/libraries/composer/platform/tests/AndroidManifest.xml b/libraries/composer/platform/tests/AndroidManifest.xml
index 1b01092..f023c7a 100644
--- a/libraries/composer/platform/tests/AndroidManifest.xml
+++ b/libraries/composer/platform/tests/AndroidManifest.xml
@@ -19,8 +19,11 @@
     <application>
         <uses-library android:name="android.test.runner"/>
     </application>
+    <uses-permission android:name="android.platform.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.platform.WRITE_EXTERNAL_STORAGE"/>
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="android.platform.test.composer.tests"
         android:label="Platform Composer Library Tests" />
 </manifest>
+
diff --git a/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java b/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java
new file mode 100644
index 0000000..83b1849
--- /dev/null
+++ b/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 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 android.platform.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.ProfileBase;
+import android.host.test.composer.ProfileTestBase;
+import android.os.Bundle;
+
+import org.junit.runner.Runner;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit test the logic for device-side {@link Profile}
+ */
+@RunWith(JUnit4.class)
+public class ProfileTest extends ProfileTestBase<Bundle> {
+    protected class TestableProfile extends ProfileBase<Bundle> {
+        @Override
+        protected Configuration getConfigurationArgument(Bundle args) {
+            return TEST_CONFIGS.get(args.getString(PROFILE_OPTION_NAME));
+        }
+    }
+
+    @Override
+    protected ProfileBase<Bundle> getProfile() {
+        return new TestableProfile();
+    }
+
+    @Override
+    protected Bundle getArguments(String configName) {
+        Bundle args = new Bundle();
+        args.putString(PROFILE_OPTION_NAME, configName);
+        return args;
+    }
+}
diff --git a/libraries/device-collectors/src/main/Android.bp b/libraries/device-collectors/src/main/Android.bp
index a32c443..c5134dc 100644
--- a/libraries/device-collectors/src/main/Android.bp
+++ b/libraries/device-collectors/src/main/Android.bp
@@ -25,3 +25,22 @@
 
     sdk_version: "current",
 }
+
+// This library enables additional listeners dependent on platform apis.
+java_library {
+    name: "collector-device-lib-platform",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: [
+        "java/**/*.java",
+        "platform-helpers/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.runner",
+        "collectors-helper",
+        "junit",
+    ],
+
+    platform_apis: true,
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java
new file mode 100644
index 0000000..a04edec
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.AppStartupHelper;
+
+/**
+ * A {@link AppStartupListener} that captures app startup during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "appstartup-collector")
+public class AppStartupListener extends BaseCollectionListener<StringBuilder> {
+    public AppStartupListener() {
+        createHelperInstance(new AppStartupHelper());
+    }
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java
new file mode 100644
index 0000000..4177635
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import com.android.helpers.ICollectorHelper;
+
+import org.junit.runner.Description;
+
+import java.util.Map;
+
+/**
+ * A {@link BaseCollectionListener} that captures metrics collected during the testing.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+public class BaseCollectionListener<T> extends BaseMetricListener {
+
+    private ICollectorHelper mHelper;
+
+
+    @Override
+    public void onTestStart(DataRecord testData, Description description) {
+        // NO-OP in the failure case for now and proceed with the testing.
+        mHelper.startCollecting();
+    }
+
+    @Override
+    public void onTestEnd(DataRecord testData, Description description) {
+        Map<String, T> metrics = mHelper.getMetrics();
+        for (Map.Entry<String, T> entry : metrics.entrySet()) {
+            testData.addStringMetric(entry.getKey(), entry.getValue().toString());
+        }
+        mHelper.stopCollecting();
+    }
+
+    protected void createHelperInstance(ICollectorHelper helper) {
+        mHelper = helper;
+    }
+
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java
new file mode 100644
index 0000000..591aed8
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.CpuUsageHelper;
+
+/**
+ * A {@link CpuUsageListener} that captures cpu usage during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "cpuusage-collector")
+public class CpuUsageListener extends BaseCollectionListener<Long> {
+    public CpuUsageListener() {
+        createHelperInstance(new CpuUsageHelper());
+    }
+}
+
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java
new file mode 100644
index 0000000..6cde413
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.CrashHelper;
+
+/**
+ * A {@link CrashListener} that captures crashes during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "crash-collector")
+public class CrashListener extends BaseCollectionListener<Integer> {
+    public CrashListener() {
+        createHelperInstance(new CrashHelper());
+    }
+}
diff --git a/libraries/junitxml/Android.bp b/libraries/junitxml/Android.bp
index b090147..464ea87 100644
--- a/libraries/junitxml/Android.bp
+++ b/libraries/junitxml/Android.bp
@@ -18,4 +18,5 @@
     name: "junitxml",
     static_libs: ["junit"],
     srcs: ["src/**/*.java"],
+    sdk_version: "core_current",
 }
diff --git a/libraries/longevity/src/android/longevity/core/LongevitySuite.java b/libraries/longevity/src/android/longevity/core/LongevitySuite.java
index a415faf..1fa46c8 100644
--- a/libraries/longevity/src/android/longevity/core/LongevitySuite.java
+++ b/libraries/longevity/src/android/longevity/core/LongevitySuite.java
@@ -19,6 +19,7 @@
 import android.longevity.core.listener.TimeoutTerminator;
 import android.host.test.composer.Iterate;
 import android.host.test.composer.Shuffle;
+import android.host.test.composer.Profile;
 
 import org.junit.runner.Runner;
 import org.junit.runner.notification.RunNotifier;
@@ -78,6 +79,7 @@
     private static List<Runner> constructClassRunners(
                 Class<?> suite, RunnerBuilder builder, Map<String, String> args)
             throws InitializationError {
+        // Note: until b/118340229 is resolved, keep the platform class method in sync as necessary.
         // Retrieve annotated suite classes.
         SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
         if (annotation == null) {
@@ -86,7 +88,7 @@
         }
         // Construct and store custom runners for the full suite.
         BiFunction<Map<String, String>, List<Runner>, List<Runner>> modifier =
-                new Iterate<Runner>().andThen(new Shuffle<Runner>());
+                new Iterate<Runner>().andThen(new Shuffle<Runner>()).andThen(new Profile());
         return modifier.apply(args, builder.runners(suite, annotation.value()));
     }
 
diff --git a/libraries/longevity/src/android/longevity/platform/LongevitySuite.java b/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
index a9f54a4..aa88961 100644
--- a/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
+++ b/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
@@ -24,13 +24,19 @@
 import android.longevity.platform.listener.TimeoutTerminator;
 import android.os.BatteryManager;
 import android.os.Bundle;
+import android.platform.test.composer.Iterate;
+import android.platform.test.composer.Shuffle;
+import android.platform.test.composer.Profile;
+import android.util.Log;
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
-import android.util.Log;
 
+import java.util.function.BiFunction;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
+import org.junit.runner.Runner;
 import org.junit.runner.notification.RunNotifier;
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.RunnerBuilder;
@@ -81,11 +87,31 @@
     public LongevitySuite(Class<?> klass, RunnerBuilder builder,
             Instrumentation instrumentation, Context context, Bundle arguments)
             throws InitializationError {
-        super(klass, builder, toMap(arguments));
+        super(klass, constructClassRunners(klass, builder, arguments), toMap(arguments));
         mInstrumentation = instrumentation;
         mContext = context;
     }
 
+    /**
+     * Constructs the sequence of {@link Runner}s using platform composers.
+     */
+    private static List<Runner> constructClassRunners(
+                Class<?> suite, RunnerBuilder builder, Bundle args)
+            throws InitializationError {
+        // TODO(b/118340229): Refactor to share logic with base class. In the meanwhile, keep the
+        // logic here in sync with the base class.
+        // Retrieve annotated suite classes.
+        SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
+        if (annotation == null) {
+            throw new InitializationError(String.format(
+                    "Longevity suite, '%s', must have a SuiteClasses annotation", suite.getName()));
+        }
+        // Construct and store custom runners for the full suite.
+        BiFunction<Bundle, List<Runner>, List<Runner>> modifier =
+                new Iterate<Runner>().andThen(new Shuffle<Runner>()).andThen(new Profile());
+        return modifier.apply(args, builder.runners(suite, annotation.value()));
+    }
+
     @Override
     public void run(final RunNotifier notifier) {
         // Register the battery terminator available only on the platform library, if present.
diff --git a/libraries/microbenchmark/Android.bp b/libraries/microbenchmark/Android.bp
index 07943fd..c9b74bd 100644
--- a/libraries/microbenchmark/Android.bp
+++ b/libraries/microbenchmark/Android.bp
@@ -18,6 +18,7 @@
     static_libs: [
         "androidx.test.runner",
         "platform-test-composers",
+        "platform-test-rules",
     ],
 
     srcs: ["src/**/*.java"],
diff --git a/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java b/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
index 9cd7068..ff1e6bb 100644
--- a/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
+++ b/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
@@ -17,6 +17,7 @@
 
 import android.os.Bundle;
 import android.platform.test.composer.Iterate;
+import android.platform.test.rule.TracePointRule;
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 
@@ -37,7 +38,7 @@
  * TightMethodRule}s in order to reliably measure a specific test method in isolation. Samples are
  * soon to follow.
  */
-public final class Microbenchmark extends BlockJUnit4ClassRunner {
+public class Microbenchmark extends BlockJUnit4ClassRunner {
     private Bundle mArguments;
 
     /**
@@ -64,6 +65,8 @@
     @Override
     protected Statement methodInvoker(FrameworkMethod method, Object test) {
         Statement start = super.methodInvoker(method, test);
+        // Wrap the inner-most test method with trace points.
+        start = getTracePointRule().apply(start, describeChild(method));
         // Invoke special @TightMethodRules that wrap @Test methods.
         List<MethodRule> tightMethodRules = getTestClass()
                 .getAnnotatedFieldValues(test, TightMethodRule.class, MethodRule.class);
@@ -73,6 +76,19 @@
         return start;
     }
 
+    @VisibleForTesting
+    protected TracePointRule getTracePointRule() {
+        return new TracePointRule();
+    }
+
+    /**
+     * Returns a list of repeated {@link FrameworkMethod}s to execute.
+     */
+    @Override
+    protected List<FrameworkMethod> getChildren() {
+       return new Iterate<FrameworkMethod>().apply(mArguments, super.getChildren());
+    }
+
     /**
      * An annotation for the corresponding tight rules above. These rules are ordered differently
      * from standard JUnit {@link Rule}s because they live between {@link Before} and {@link After}
@@ -99,12 +115,4 @@
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD, ElementType.METHOD})
     public @interface TightMethodRule { }
-
-    /**
-     * Returns a list of repeated {@link FrameworkMethod}s to execute.
-     */
-    @Override
-    protected List<FrameworkMethod> getChildren() {
-       return new Iterate<FrameworkMethod>().apply(mArguments, super.getChildren());
-    }
 }
diff --git a/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java b/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
index 83897ec..cafd229 100644
--- a/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
+++ b/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
@@ -20,11 +20,13 @@
 import static org.junit.Assert.fail;
 
 import android.os.Bundle;
+import android.platform.test.rule.TracePointRule;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.rules.MethodRule;
+import org.junit.runner.Description;
 import org.junit.runner.JUnitCore;
 import org.junit.runner.Result;
 import org.junit.runner.RunWith;
@@ -33,6 +35,9 @@
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.Statement;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Unit tests for the {@link Microbenchmark} runner.
  */
@@ -55,43 +60,107 @@
     }
 
     /**
-     * Tests that {@link TightMethodRule}s are ordered properly.
+     * Tests that {@link TracePointRule} and {@link TightMethodRule}s are properly ordered.
      *
-     * <p>@Before --> @Tight Before --> @Test --> @Tight After --> @After.
+     * Before --> TightBefore --> Trace (begin) --> Test --> Trace(end) --> TightAfter --> After
      */
     @Test
-    public void testTightMethodRuleOrder() {
-        try {
-            Result result = new JUnitCore().run(OrderTest.class);
-            assertThat(result.wasSuccessful()).isTrue();
-            assertThat(result.getRunCount()).isAtLeast(1);
-        } catch (Exception e) {
-            fail("This test should not throw.");
+    public void testFeatureExecutionOrder() throws InitializationError {
+        LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class);
+        loggingRunner.setOperationLog(new ArrayList<String>());
+        Result result = new JUnitCore().run(loggingRunner);
+        assertThat(result.wasSuccessful()).isTrue();
+        assertThat(loggingRunner.getOperationLog()).containsExactly(
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest)",
+                "test",
+                "end",
+                "tight after",
+                "after")
+            .inOrder();
+    }
+
+    /**
+     * An extensions of the {@link Microbenchmark} runner that logs the start and end of collecting
+     * traces. It also passes the operation log to the provided test {@code Class}, if it is a
+     * {@link LoggingTest}. This is used for ensuring the proper order for evaluating test {@link
+     * Statement}s.
+     */
+    public static class LoggingMicrobenchmark extends Microbenchmark {
+        private List<String> mOperationLog;
+
+        public LoggingMicrobenchmark(Class<?> klass) throws InitializationError {
+            super(klass);
+        }
+
+        LoggingMicrobenchmark(Class<?> klass, Bundle arguments) throws InitializationError {
+            super(klass, arguments);
+        }
+
+        protected Object createTest() throws Exception {
+            Object test = super.createTest();
+            if (test instanceof LoggingTest) {
+                ((LoggingTest)test).setOperationLog(mOperationLog);
+            }
+            return test;
+        }
+
+        void setOperationLog(List<String> log) {
+            mOperationLog = log;
+        }
+
+        List<String> getOperationLog() {
+            return mOperationLog;
+        }
+
+        @Override
+        protected TracePointRule getTracePointRule() {
+            return new LoggingTracePointRule();
+        }
+
+        class LoggingTracePointRule extends TracePointRule {
+            @Override
+            protected void beginSection(String sectionTag) {
+                mOperationLog.add(String.format("begin: %s", sectionTag));
+            }
+
+            @Override
+            protected void endSection() {
+                mOperationLog.add("end");
+            }
         }
     }
 
-    @RunWith(Microbenchmark.class)
-    public static class OrderTest {
+    /**
+     * A test that logs {@link Before}, {@link After}, {@link Test}, and the logging {@link
+     * TightMethodRule} included, used in conjunction with {@link LoggingMicrobenchmark} to
+     * determine all {@link Statement}s are evaluated in the proper order.
+     */
+    public static class LoggingTest {
         @Microbenchmark.TightMethodRule
         public TightRule orderRule = new TightRule();
 
-        private boolean hasCalledBefore = false;
-        private boolean hasCalledAfter = false;
-        private boolean hasCalledTest = false;
+        private List<String> mOperationLog;
+
+        void setOperationLog(List<String> log) {
+            mOperationLog = log;
+        }
 
         @Before
         public void beforeMethod() {
-            hasCalledBefore = true;
+            mOperationLog.add("before");
         }
 
         @Test
         public void testMethod() {
-            hasCalledTest = true;
+            mOperationLog.add("test");
         }
 
         @After
         public void afterMethod() {
-            hasCalledAfter = true;
+            mOperationLog.add("after");
         }
 
         class TightRule implements MethodRule {
@@ -100,16 +169,9 @@
                 return new Statement() {
                     @Override
                     public void evaluate() throws Throwable {
-                        // Tight before statement.
-                        assertWithMessage("Before was not called before rule evaluation.")
-                                .that(hasCalledBefore).isTrue();
-                        // Test method evaluation.
+                        mOperationLog.add("tight before");
                         base.evaluate();
-                        // Tight after statement.
-                        assertWithMessage("Test not called before base evaluation.")
-                                .that(hasCalledTest).isTrue();
-                        assertWithMessage("After was called before rule evaluation.")
-                                .that(hasCalledAfter).isFalse();
+                        mOperationLog.add("tight after");
                     }
                 };
             }
diff --git a/tests/jank/jankmicrobenchmark/Android.bp b/tests/jank/jankmicrobenchmark/Android.bp
index 3809958..c32b4df 100644
--- a/tests/jank/jankmicrobenchmark/Android.bp
+++ b/tests/jank/jankmicrobenchmark/Android.bp
@@ -27,6 +27,4 @@
     libs: ["android.test.base.stubs"],
 
     sdk_version: "test_current",
-
-    test_suites: ["device-tests"],
 }
diff --git a/tests/jank/jankmicrobenchmark/AndroidTest.xml b/tests/jank/jankmicrobenchmark/AndroidTest.xml
deleted file mode 100644
index d1523f6..0000000
--- a/tests/jank/jankmicrobenchmark/AndroidTest.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<configuration description="Runs Platform Benchmark Jank Tests.">
-
-    <target_preparer class="com.google.android.tradefed.targetprep.GoogleDeviceSetup">
-        <option name="screen-saver" value="off" />
-        <option name="location-gps" value="off" />
-        <option name="location-network" value="off" />
-        <option name="battery-saver-mode" value="off" />
-        <option name="disable-playstore" value="true" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="JankMicroBenchmarkTests.apk" />
-        <option name="test-file-name" value="ApiDemos.apk" />
-    </target_preparer>
-
-    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
-    <option name="test-tag" value="JankMicroBenchmarkTests" />
-    <test class="com.android.tradefed.testtype.InstrumentationTest" >
-        <option name="package" value="com.android.jankmicrobenchmark.janktests" />
-        <option name="runner" value="android.test.InstrumentationTestRunner" />
-        <option name="test-timeout" value="1800000" />
-        <option name="shell-timeout" value="1800000" />
-    </test>
-</configuration>