Merge "Change sl4a request/response log message to verbose"
diff --git a/atest/atest_arg_parser.py b/atest/atest_arg_parser.py
index e4dee5f..0a6e615 100644
--- a/atest/atest_arg_parser.py
+++ b/atest/atest_arg_parser.py
@@ -312,9 +312,6 @@
         --collect-tests-only
             {COLLECT_TESTS_ONLY}
 
-        --dry-run
-            {DRY_RUN}
-
         --info
             {INFO}
 
@@ -329,6 +326,9 @@
 
 
         [ Dry-Run and Caching ]
+        --dry-run
+            {DRY_RUN}
+
         -c, --clear-cache
             {CLEAR_CACHE}
 
diff --git a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index 9ec2270..66f3f3b 100644
--- a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -125,6 +125,16 @@
     public boolean setProperty(String propKey, String propValue) throws DeviceNotAvailableException;
 
     /**
+     * Retrieve the given fastboot variable value from the device.
+     *
+     * @param variableName the variable name
+     * @return the property value or <code>null</code> if it does not exist
+     * @throws DeviceNotAvailableException, UnsupportedOperationException
+     */
+    public String getFastbootVariable(String variableName)
+            throws DeviceNotAvailableException, UnsupportedOperationException;
+
+    /**
      * Convenience method to get the bootloader version of this device.
      * <p/>
      * Will attempt to retrieve bootloader version from the device's current state. (ie if device
@@ -569,6 +579,13 @@
     public boolean isRuntimePermissionSupported() throws DeviceNotAvailableException;
 
     /**
+     * Check whether platform on device supports app enumeration
+     * @return True if app enumeration is supported, false otherwise
+     * @throws DeviceNotAvailableException
+     */
+    public boolean isAppEnumerationSupported() throws DeviceNotAvailableException;
+
+    /**
      * Retrieves a file off device.
      *
      * @param remoteFilePath the absolute path to file on device.
@@ -925,6 +942,16 @@
     public void reboot(@Nullable String reason) throws DeviceNotAvailableException;
 
     /**
+     * Reboots the device into fastbootd mode.
+     *
+     * <p>Blocks until device is in fastbootd mode.
+     *
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    public void rebootIntoFastbootd() throws DeviceNotAvailableException;
+
+    /**
      * Reboots only userspace part of device.
      *
      * <p>Blocks until device becomes available.
diff --git a/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java b/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
index 5066999..5ce00ec 100644
--- a/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
@@ -21,9 +21,9 @@
  * A placeholder {@link IDevice} used by {@link DeviceManager} to allocate when {@link
  * DeviceSelectionOptions#localVirtualDeviceRequested()} is <code>true</code>
  */
-public class StubLocalAndroidVirtualDevice extends StubDevice {
+public class StubLocalAndroidVirtualDevice extends TcpDevice {
 
     public StubLocalAndroidVirtualDevice(String serial) {
-        super(serial, false);
+        super(serial);
     }
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index 055cc73..3e37f20 100644
--- a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -276,9 +276,11 @@
     public static String createEscapedContentUri(String deviceFilePath) {
         String escapedFilePath = deviceFilePath;
         try {
-            // Escape the path then encode it.
-            String escaped = UrlEscapers.urlPathSegmentEscaper().escape(deviceFilePath);
-            escapedFilePath = URLEncoder.encode(escaped, "UTF-8");
+            // Encode the path then escape it. This logic must invert the logic in
+            // ManagedFileContentProvider.getFileForUri. That calls to Uri.getPath() and then
+            // URLDecoder.decode(), so this must invert each of those two steps and switch the order
+            String encoded = URLEncoder.encode(deviceFilePath, "UTF-8");
+            escapedFilePath = UrlEscapers.urlPathSegmentEscaper().escape(encoded);
         } catch (UnsupportedEncodingException e) {
             CLog.e(e);
         }
diff --git a/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
index 537daf4..5603712 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.util.UniqueMultiMap;
 
 import java.io.Serializable;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -36,6 +37,7 @@
     /** Key used for storing associated invocation ID. */
     public static final String INVOCATION_ID = "invocation-id";
 
+    @Deprecated
     public enum TimingEvent {
         FETCH_BUILD,
         SETUP;
@@ -145,10 +147,16 @@
     public MultiMap<String, String> getAttributes();
 
     /** Add a invocation timing metric. */
-    public void addInvocationTimingMetric(TimingEvent timingEvent, Long durationMillis);
+    @Deprecated
+    public default void addInvocationTimingMetric(TimingEvent timingEvent, Long durationMillis) {
+        // Do nothing
+    }
 
     /** Returns the map containing the invocation timing metrics. */
-    public Map<TimingEvent, Long> getInvocationTimingMetrics();
+    @Deprecated
+    public default Map<TimingEvent, Long> getInvocationTimingMetrics() {
+        return new HashMap<>();
+    }
 
     /** Sets the descriptor associated with the test configuration that launched the invocation */
     public void setConfigurationDescriptor(ConfigurationDescriptor configurationDescriptor);
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
index 42eec08..4427fe5 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
@@ -87,7 +87,9 @@
     /** Clear the invocation info for an invocation. */
     public static void clearInvocationInfos() {
         ThreadGroup group = Thread.currentThread().getThreadGroup();
-        mPerGroupInfo.remove(group);
+        synchronized (mPerGroupInfo) {
+            mPerGroupInfo.remove(group);
+        }
     }
 
     /**
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index f9718cb..8b8b802 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -137,6 +137,8 @@
     /** Clear the invocation metrics for an invocation. */
     public static void clearInvocationMetrics() {
         ThreadGroup group = Thread.currentThread().getThreadGroup();
-        mPerGroupMetrics.remove(group);
+        synchronized (mPerGroupMetrics) {
+            mPerGroupMetrics.remove(group);
+        }
     }
 }
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index f9d558a..fa09182 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -17,6 +17,7 @@
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.cluster.ClusterHostEvent.HostEventType;
 import com.android.tradefed.command.CommandScheduler;
 import com.android.tradefed.command.ICommandScheduler;
 import com.android.tradefed.command.remote.DeviceDescriptor;
@@ -33,6 +34,7 @@
 import com.android.tradefed.device.battery.IBatteryInfo.BatteryState;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestSummaryListener;
@@ -42,7 +44,6 @@
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.QuotationAwareTokenizer;
 
-import com.android.tradefed.cluster.ClusterHostEvent.HostEventType;
 import com.google.common.primitives.Ints;
 
 import org.json.JSONException;
@@ -56,7 +57,6 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.RejectedExecutionHandler;
@@ -289,20 +289,17 @@
                 mError = "build not found";
             }
 
-            long fetchBuildTimeMillis = -1;
-            long setupTimeMillis = -1;
-            if (metadata != null && metadata.getInvocationTimingMetrics() != null) {
-                for (Entry<IInvocationContext.TimingEvent, Long> entry :
-                        metadata.getInvocationTimingMetrics().entrySet()) {
-                    switch (entry.getKey()) {
-                        case FETCH_BUILD:
-                            fetchBuildTimeMillis = entry.getValue();
-                            break;
-                        case SETUP:
-                            setupTimeMillis = entry.getValue();
-                            break;
-                    }
-                }
+            String fetchBuildTimeMillis = "-1";
+            String setupTimeMillis = "-1";
+            if (metadata != null) {
+                fetchBuildTimeMillis =
+                        metadata.getAttributes()
+                                .getUniqueMap()
+                                .get(InvocationMetricKey.FETCH_BUILD.toString());
+                setupTimeMillis =
+                        metadata.getAttributes()
+                                .getUniqueMap()
+                                .get(InvocationMetricKey.SETUP.toString());
             }
 
             // Stop heartbeat thread before sending InvocationCompleted event.
@@ -318,10 +315,9 @@
                             .setData(ClusterCommandEvent.DATA_KEY_SUMMARY, mSummary)
                             .setData(
                                     ClusterCommandEvent.DATA_KEY_FETCH_BUILD_TIME_MILLIS,
-                                    Long.toString(fetchBuildTimeMillis))
+                                    fetchBuildTimeMillis)
                             .setData(
-                                    ClusterCommandEvent.DATA_KEY_SETUP_TIME_MILLIS,
-                                    Long.toString(setupTimeMillis))
+                                    ClusterCommandEvent.DATA_KEY_SETUP_TIME_MILLIS, setupTimeMillis)
                             .setData(
                                     ClusterCommandEvent.DATA_KEY_TOTAL_TEST_COUNT,
                                     Integer.toString(getNumTotalTests()))
diff --git a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
index cce0bbf..66a92fb 100644
--- a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
@@ -20,6 +20,7 @@
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.cloud.GceAvdInfo;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -36,6 +37,7 @@
 import com.android.tradefed.util.ZipUtil;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -43,7 +45,9 @@
 import java.util.List;
 
 /** The class for local virtual devices running on TradeFed host. */
-public class LocalAndroidVirtualDevice extends TestDevice implements ITestLoggerReceiver {
+public class LocalAndroidVirtualDevice extends RemoteAndroidDevice implements ITestLoggerReceiver {
+
+    private static final int INVALID_PORT = 0;
 
     // Environment variables.
     private static final String ANDROID_HOST_OUT = "ANDROID_HOST_OUT";
@@ -53,13 +57,8 @@
     // The name of the GZIP file containing launch_cvd and stop_cvd.
     private static final String CVD_HOST_PACKAGE_NAME = "cvd-host_package.tar.gz";
 
-    // The port of cuttlefish instance 1.
-    private static final int CUTTLEFISH_FIRST_HOST_PORT = 6520;
-
     private static final String ACLOUD_CVD_TEMP_DIR_NAME = "acloud_cvd_temp";
-    private static final String INSTANCE_DIR_NAME_PREFIX = "instance_home_";
     private static final String CUTTLEFISH_RUNTIME_DIR_NAME = "cuttlefish_runtime";
-    private static final String INSTANCE_NAME_PREFIX = "local-instance-";
 
     private ITestLogger mTestLogger = null;
 
@@ -68,11 +67,7 @@
     private boolean mShouldDeleteHostPackageDir = false;
     private File mImageDir = null;
 
-    // The data for restoring the stub device at tear-down.
-    private String mOriginalSerialNumber = null;
-
-    // A positive integer for acloud to identify this device.
-    private int mInstanceId = -1;
+    private GceAvdInfo mGceAvdInfo = null;
 
     public LocalAndroidVirtualDevice(
             IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
@@ -86,27 +81,58 @@
         // The setup method in super class does not require the device to be online.
         super.preInvocationSetup(info, testResourceBuildInfos);
 
-        // TODO(b/133211308): multiple instances
-        mInstanceId = 1;
-        replaceStubDevice("127.0.0.1:" + (CUTTLEFISH_FIRST_HOST_PORT + mInstanceId - 1));
-
         createTempDirs((IDeviceBuildInfo) info);
 
-        CommandResult result = acloudCreate(info.getBuildFlavor(), getOptions());
+        CommandResult result = null;
+        File report = null;
+        try {
+            report = FileUtil.createTempFile("report", ".json");
+            result = acloudCreate(info.getBuildFlavor(), report, getOptions());
+            loadAvdInfo(report);
+        } catch (IOException ex) {
+            throw new TargetSetupError(
+                    "Cannot create acloud report file.", ex, getDeviceDescriptor());
+        } finally {
+            FileUtil.deleteFile(report);
+        }
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
             throw new TargetSetupError(
-                    String.format("Cannot launch virtual device. stderr:\n%s", result.getStderr()),
+                    String.format("Cannot execute acloud command. stderr:\n%s", result.getStderr()),
                     getDeviceDescriptor());
         }
+
+        HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
+        replaceStubDevice(hostAndPort.toString());
+
+        RecoveryMode previousMode = getRecoveryMode();
+        try {
+            setRecoveryMode(RecoveryMode.NONE);
+            if (!adbTcpConnect(hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
+                throw new TargetSetupError(
+                        String.format("Cannot connect to %s.", hostAndPort), getDeviceDescriptor());
+            }
+            waitForDeviceAvailable();
+        } finally {
+            setRecoveryMode(previousMode);
+        }
     }
 
     /** Execute common tear-down procedure and stop the virtual device. */
     @Override
     public void postInvocationTearDown(Throwable exception) {
         TestDeviceOptions options = getOptions();
+        HostAndPort hostAndPort = getHostAndPortFromAvdInfo();
+        String instanceName = (mGceAvdInfo != null ? mGceAvdInfo.instanceName() : null);
         try {
-            if (!options.shouldSkipTearDown() && mHostPackageDir != null) {
-                CommandResult result = acloudDelete(options);
+            if (!options.shouldSkipTearDown() && hostAndPort != null) {
+                if (!adbTcpDisconnect(
+                        hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
+                    CLog.e("Cannot disconnect from %s", hostAndPort.toString());
+                }
+            }
+
+            if (!options.shouldSkipTearDown() && instanceName != null) {
+                CommandResult result = acloudDelete(instanceName, options);
                 if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
                     CLog.e("Cannot stop the virtual device.");
                 }
@@ -114,7 +140,9 @@
                 CLog.i("Skip stopping the virtual device.");
             }
 
-            reportInstanceLogs();
+            if (instanceName != null) {
+                reportInstanceLogs(instanceName);
+            }
         } finally {
             restoreStubDevice();
 
@@ -122,10 +150,15 @@
                 deleteTempDirs();
             } else {
                 CLog.i(
-                        "Skip deleting the temporary directories.\nHost package: %s\nImage: %s\n",
-                        mHostPackageDir, mImageDir);
+                        "Skip deleting the temporary directories.\n"
+                                + "Address: %s\nName: %s\nHost package: %s\nImage: %s",
+                        hostAndPort, instanceName, mHostPackageDir, mImageDir);
+                mHostPackageDir = null;
+                mImageDir = null;
             }
 
+            mGceAvdInfo = null;
+
             super.postInvocationTearDown(exception);
         }
     }
@@ -202,38 +235,14 @@
             throw new TargetSetupError(
                     "Unexpected device type: " + device.getClass(), getDeviceDescriptor());
         }
-        mOriginalSerialNumber = device.getSerialNumber();
         setIDevice(new StubLocalAndroidVirtualDevice(newSerialNumber));
         setFastbootEnabled(false);
     }
 
-    /**
-     * Set this device to be offline and associate it with a {@link StubLocalAndroidVirtualDevice}.
-     */
+    /** Restore the {@link StubLocalAndroidVirtualDevice} with the initial serial number. */
     private void restoreStubDevice() {
-        if (mOriginalSerialNumber == null) {
-            CLog.w("Skip restoring the stub device.");
-            return;
-        }
-        setIDevice(new StubLocalAndroidVirtualDevice(mOriginalSerialNumber));
+        setIDevice(new StubLocalAndroidVirtualDevice(getInitialSerial()));
         setFastbootEnabled(false);
-        mOriginalSerialNumber = null;
-    }
-
-    private String getInstanceDirName() {
-        return INSTANCE_DIR_NAME_PREFIX + mInstanceId;
-    }
-
-    private File getInstanceDir() {
-        return FileUtil.getFileForPath(
-                getTmpDir(),
-                ACLOUD_CVD_TEMP_DIR_NAME,
-                getInstanceDirName(),
-                CUTTLEFISH_RUNTIME_DIR_NAME);
-    }
-
-    private String getInstanceName() {
-        return INSTANCE_NAME_PREFIX + mInstanceId;
     }
 
     private static void addLogLevelToAcloudCommand(List<String> command, LogLevel logLevel) {
@@ -244,7 +253,7 @@
         }
     }
 
-    private CommandResult acloudCreate(String buildFlavor, TestDeviceOptions options) {
+    private CommandResult acloudCreate(String buildFlavor, File report, TestDeviceOptions options) {
         CommandResult result = null;
 
         File acloud = options.getAvdDriverBinary();
@@ -262,6 +271,7 @@
                             options.getGceCmdTimeout(),
                             acloud,
                             buildFlavor,
+                            report,
                             options.getGceDriverLogLevel(),
                             options.getGceDriverParams());
             if (CommandStatus.SUCCESS.equals(result.getStatus())) {
@@ -275,18 +285,16 @@
     }
 
     private CommandResult acloudCreate(
-            long timeout, File acloud, String buildFlavor, LogLevel logLevel, List<String> args) {
+            long timeout,
+            File acloud,
+            String buildFlavor,
+            File report,
+            LogLevel logLevel,
+            List<String> args) {
         IRunUtil runUtil = createRunUtil();
         // The command creates the instance directory under TMPDIR.
         runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath());
-        // The command finds bin/launch_cvd in ANDROID_HOST_OUT.
-        runUtil.setEnvVariable(ANDROID_HOST_OUT, mHostPackageDir.getAbsolutePath());
         runUtil.setEnvVariable(TARGET_PRODUCT, buildFlavor);
-        // TODO(b/141349771): Size of sockaddr_un->sun_path is 108, which may be too small for this
-        // path.
-        if (new File(getInstanceDir(), "launcher_monitor.sock").getAbsolutePath().length() > 108) {
-            CLog.w("Length of instance path is too long for launch_cvd.");
-        }
 
         List<String> command =
                 new ArrayList<String>(
@@ -294,9 +302,13 @@
                                 acloud.getAbsolutePath(),
                                 "create",
                                 "--local-instance",
-                                Integer.toString(mInstanceId),
                                 "--local-image",
                                 mImageDir.getAbsolutePath(),
+                                "--local-tool",
+                                mHostPackageDir.getAbsolutePath(),
+                                "--report_file",
+                                report.getAbsolutePath(),
+                                "--no-autoconnect",
                                 "--yes",
                                 "--skip-pre-run-check"));
         addLogLevelToAcloudCommand(command, logLevel);
@@ -308,7 +320,47 @@
         return result;
     }
 
-    private CommandResult acloudDelete(TestDeviceOptions options) {
+    /**
+     * Get valid host and port from mGceAvdInfo.
+     *
+     * @return {@link HostAndPort} if the port is valid; null otherwise.
+     */
+    private HostAndPort getHostAndPortFromAvdInfo() {
+        if (mGceAvdInfo == null) {
+            return null;
+        }
+        HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
+        if (hostAndPort == null
+                || !hostAndPort.hasPort()
+                || hostAndPort.getPort() == INVALID_PORT) {
+            return null;
+        }
+        return hostAndPort;
+    }
+
+    /** Initialize instance name, host address, and port from an acloud report file. */
+    private void loadAvdInfo(File report) throws TargetSetupError {
+        mGceAvdInfo = GceAvdInfo.parseGceInfoFromFile(report, getDeviceDescriptor(), INVALID_PORT);
+        if (mGceAvdInfo == null) {
+            throw new TargetSetupError("Cannot read acloud report file.", getDeviceDescriptor());
+        }
+
+        if (Strings.isNullOrEmpty(mGceAvdInfo.instanceName())) {
+            throw new TargetSetupError("No instance name in acloud report.", getDeviceDescriptor());
+        }
+
+        if (getHostAndPortFromAvdInfo() == null) {
+            throw new TargetSetupError("No port in acloud report.", getDeviceDescriptor());
+        }
+
+        if (!GceAvdInfo.GceStatus.SUCCESS.equals(mGceAvdInfo.getStatus())) {
+            throw new TargetSetupError(
+                    "Cannot launch virtual device: " + mGceAvdInfo.getErrors(),
+                    getDeviceDescriptor());
+        }
+    }
+
+    private CommandResult acloudDelete(String instanceName, TestDeviceOptions options) {
         File acloud = options.getAvdDriverBinary();
         if (acloud == null || !acloud.isFile()) {
             CLog.e("Specified AVD driver binary is not a file.");
@@ -324,8 +376,9 @@
                         Arrays.asList(
                                 acloud.getAbsolutePath(),
                                 "delete",
+                                "--local-only",
                                 "--instance-names",
-                                getInstanceName()));
+                                instanceName));
         addLogLevelToAcloudCommand(command, options.getGceDriverLogLevel());
 
         CommandResult result =
@@ -335,24 +388,29 @@
         return result;
     }
 
-    private void reportInstanceLogs() {
+    private void reportInstanceLogs(String instanceName) {
         if (mTestLogger == null) {
             return;
         }
-        reportInstanceLog("kernel.log", LogDataType.KERNEL_LOG);
-        reportInstanceLog("logcat", LogDataType.LOGCAT);
-        reportInstanceLog("launcher.log", LogDataType.TEXT);
-        reportInstanceLog("cuttlefish_config.json", LogDataType.TEXT);
+        File instanceDir =
+                FileUtil.getFileForPath(
+                        getTmpDir(),
+                        ACLOUD_CVD_TEMP_DIR_NAME,
+                        instanceName,
+                        CUTTLEFISH_RUNTIME_DIR_NAME);
+        reportInstanceLog(new File(instanceDir, "kernel.log"), LogDataType.KERNEL_LOG);
+        reportInstanceLog(new File(instanceDir, "logcat"), LogDataType.LOGCAT);
+        reportInstanceLog(new File(instanceDir, "launcher.log"), LogDataType.TEXT);
+        reportInstanceLog(new File(instanceDir, "cuttlefish_config.json"), LogDataType.TEXT);
     }
 
-    private void reportInstanceLog(String fileName, LogDataType type) {
-        File file = new File(getInstanceDir(), fileName);
+    private void reportInstanceLog(File file, LogDataType type) {
         if (file.exists()) {
             try (InputStreamSource source = new FileInputStreamSource(file)) {
-                mTestLogger.testLog(fileName, type, source);
+                mTestLogger.testLog(file.getName(), type, source);
             }
         } else {
-            CLog.w("%s doesn't exist.", fileName);
+            CLog.w("%s doesn't exist.", file.getAbsolutePath());
         }
     }
 
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index ee6c71d..1432b1b 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -632,7 +632,9 @@
         return prop;
     }
 
-    private String getFastbootVariable(String variableName)
+    /** {@inheritDoc} */
+    @Override
+    public String getFastbootVariable(String variableName)
             throws DeviceNotAvailableException, UnsupportedOperationException {
         CommandResult result = executeFastbootCommand("getvar", variableName);
         if (result.getStatus() == CommandStatus.SUCCESS) {
@@ -993,6 +995,14 @@
     }
 
     /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isAppEnumerationSupported() throws DeviceNotAvailableException {
+        return getApiLevel() > 29;
+    }
+
+    /**
      * helper method to throw exception if runtime permission isn't supported
      * @throws DeviceNotAvailableException
      */
@@ -2895,21 +2905,43 @@
     @Override
     public void rebootIntoBootloader()
             throws DeviceNotAvailableException, UnsupportedOperationException {
+        rebootIntoFastbootInternal(true);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void rebootIntoFastbootd()
+            throws DeviceNotAvailableException, UnsupportedOperationException {
+        rebootIntoFastbootInternal(false);
+    }
+
+    /**
+     * Reboots the device into bootloader or fastbootd mode.
+     *
+     * @param isBootloader: true to boot the device into bootloader mode, false to boot the device
+     *     into fastbootd mode.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    private void rebootIntoFastbootInternal(boolean isBootloader)
+            throws DeviceNotAvailableException {
+        final RebootMode mode =
+                isBootloader ? RebootMode.REBOOT_INTO_BOOTLOADER : RebootMode.REBOOT_INTO_FASTBOOT;
         if (!mFastbootEnabled) {
             throw new UnsupportedOperationException(
-                    "Fastboot is not available and cannot reboot into bootloader");
+                    String.format("Fastboot is not available and cannot reboot into %s", mode));
         }
         // If we go to bootloader, it's probably for flashing so ensure we re-check the provider
         mShouldSkipContentProviderSetup = false;
         CLog.i(
-                "Rebooting device %s in state %s into bootloader",
-                getSerialNumber(), getDeviceState());
+                "Rebooting device %s in state %s into %s",
+                getSerialNumber(), getDeviceState(), mode);
         if (TestDeviceState.FASTBOOT.equals(getDeviceState())) {
             CLog.i("device %s already in fastboot. Rebooting anyway", getSerialNumber());
-            executeFastbootCommand("reboot-bootloader");
+            executeFastbootCommand(String.format("reboot-%s", mode));
         } else {
-            CLog.i("Booting device %s into bootloader", getSerialNumber());
-            doAdbRebootBootloader();
+            CLog.i("Booting device %s into %s", getSerialNumber(), mode);
+            doAdbReboot(mode, null);
         }
         if (!mStateMonitor.waitForDeviceBootloader(mOptions.getFastbootTimeout())) {
             recoverDeviceFromBootloader();
@@ -3083,6 +3115,11 @@
                 return Strings.isNullOrEmpty(reason) ? mRebootTarget : mRebootTarget + "," + reason;
             }
         }
+
+        @Override
+        public String toString() {
+            return mRebootTarget;
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/invoker/InvocationContext.java b/src/com/android/tradefed/invoker/InvocationContext.java
index d4cdeda..0dcd3fb 100644
--- a/src/com/android/tradefed/invoker/InvocationContext.java
+++ b/src/com/android/tradefed/invoker/InvocationContext.java
@@ -51,7 +51,6 @@
     private Map<String, IBuildInfo> mNameAndBuildinfoMap;
     private final UniqueMultiMap<String, String> mInvocationAttributes =
             new UniqueMultiMap<String, String>();
-    private Map<IInvocationContext.TimingEvent, Long> mInvocationTimingMetrics;
     /** Invocation test-tag **/
     private String mTestTag;
     /** configuration descriptor */
@@ -70,7 +69,6 @@
      * Creates a {@link BuildInfo} using default attribute values.
      */
     public InvocationContext() {
-        mInvocationTimingMetrics = new LinkedHashMap<>();
         mAllocatedDeviceAndBuildMap = new LinkedHashMap<ITestDevice, IBuildInfo>();
         // Use LinkedHashMap to ensure key ordering by insertion order
         mNameAndDeviceMap = new LinkedHashMap<String, ITestDevice>();
@@ -229,22 +227,6 @@
         return copy;
     }
 
-
-    /** {@inheritDoc} */
-    @Override
-    public Map<IInvocationContext.TimingEvent, Long> getInvocationTimingMetrics() {
-        return mInvocationTimingMetrics;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void addInvocationTimingMetric(IInvocationContext.TimingEvent timingEvent,
-            Long durationMillis) {
-        mInvocationTimingMetrics.put(timingEvent, durationMillis);
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -375,10 +357,6 @@
         // now we are a "live" object again, so let's init the transient field
         mAllocatedDeviceAndBuildMap = new LinkedHashMap<ITestDevice, IBuildInfo>();
         mNameAndDeviceMap = new LinkedHashMap<String, ITestDevice>();
-        // For compatibility, when parent TF does not have the invocation timing yet.
-        if (mInvocationTimingMetrics == null) {
-            mInvocationTimingMetrics = new LinkedHashMap<>();
-        }
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index 233b75e..5fb9acd 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -255,8 +255,6 @@
             // Note: These metrics are handled in a try in case of a kernel reset or device issue.
             // Setup timing metric. It does not include flashing time on boot tests.
             long setupDuration = System.currentTimeMillis() - start;
-            testInfo.getContext()
-                    .addInvocationTimingMetric(IInvocationContext.TimingEvent.SETUP, setupDuration);
             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP, setupDuration);
             CLog.d("Setup duration: %s'", TimeUtil.formatElapsedTime(setupDuration));
             // Upload the setup logcat after setup is complete.
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index b84a2ed..61754c9 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -38,6 +38,7 @@
 import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.invoker.shard.LastShardDetector;
@@ -402,6 +403,7 @@
                     }
                 }
             } finally {
+                TfObjectTracker.clearTracking();
                 CurrentInvocation.clearInvocationInfos();
                 invocationPath.cleanUpBuilds(context, config);
             }
@@ -830,8 +832,6 @@
             boolean providerSuccess =
                     invokeFetchBuild(info, config, rescheduler, listener, invocationPath);
             long fetchBuildDuration = System.currentTimeMillis() - start;
-            context.addInvocationTimingMetric(IInvocationContext.TimingEvent.FETCH_BUILD,
-                    fetchBuildDuration);
             InvocationMetricLogger.addInvocationMetrics(
                     InvocationMetricKey.FETCH_BUILD, fetchBuildDuration);
             CLog.d("Fetch build duration: %s", TimeUtil.formatElapsedTime(fetchBuildDuration));
diff --git a/src/com/android/tradefed/invoker/logger/TfObjectTracker.java b/src/com/android/tradefed/invoker/logger/TfObjectTracker.java
new file mode 100644
index 0000000..cdb79ef
--- /dev/null
+++ b/src/com/android/tradefed/invoker/logger/TfObjectTracker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 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.tradefed.invoker.logger;
+
+import com.android.tradefed.build.IBuildProvider;
+import com.android.tradefed.device.metric.IMetricCollector;
+import com.android.tradefed.postprocessor.IPostProcessor;
+import com.android.tradefed.suite.checker.ISystemStatusChecker;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** A utility to track the usage of the different Trade Fedederation objects. */
+public class TfObjectTracker {
+
+    public static final String TF_OBJECTS_TRACKING_KEY = "tf_objects_tracking";
+    private static final Set<Class<?>> TRACKED_CLASSES =
+            ImmutableSet.of(
+                    IBuildProvider.class,
+                    IMetricCollector.class,
+                    IMultiTargetPreparer.class,
+                    IPostProcessor.class,
+                    IRemoteTest.class,
+                    ISystemStatusChecker.class,
+                    ITargetPreparer.class);
+
+    private TfObjectTracker() {}
+
+    private static final Map<ThreadGroup, Map<String, Long>> mPerGroupUsage =
+            new ConcurrentHashMap<ThreadGroup, Map<String, Long>>();
+
+    /** Count the occurrence of a give class and its super classes until the Tradefed interface. */
+    public static void countWithParents(Class<?> object) {
+        if (!count(object)) {
+            return;
+        }
+        // Track all the super class until not a TF interface to get a full picture.
+        countWithParents(object.getSuperclass());
+    }
+
+    /**
+     * Count the current occurrence only if it's part of the tracked objects.
+     *
+     * @param object The object to track
+     * @return True if the object was tracked, false otherwise.
+     */
+    public static boolean count(Class<?> object) {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        String qualifiedName = object.getName();
+
+        boolean tracked = false;
+        for (Class<?> classTracked : TRACKED_CLASSES) {
+            if (classTracked.isAssignableFrom(object)) {
+                tracked = true;
+                break;
+            }
+        }
+        if (!tracked) {
+            return false;
+        }
+        if (mPerGroupUsage.get(group) == null) {
+            mPerGroupUsage.put(group, new ConcurrentHashMap<>());
+        }
+        Map<String, Long> countMap = mPerGroupUsage.get(group);
+        long count = 0;
+        if (countMap.get(qualifiedName) != null) {
+            count = countMap.get(qualifiedName);
+        }
+        count++;
+        countMap.put(qualifiedName, count);
+        return true;
+    }
+
+    /** Returns the usage of the tracked objects. */
+    public static Map<String, Long> getUsage() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        if (mPerGroupUsage.get(group) == null) {
+            mPerGroupUsage.put(group, new ConcurrentHashMap<>());
+        }
+        return new HashMap<>(mPerGroupUsage.get(group));
+    }
+
+    /** Stop tracking the current invocation. This is called automatically by the harness. */
+    public static void clearTracking() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        mPerGroupUsage.remove(group);
+    }
+}
diff --git a/src/com/android/tradefed/postprocessor/BasePostProcessor.java b/src/com/android/tradefed/postprocessor/BasePostProcessor.java
index 89a1d18..983d435 100644
--- a/src/com/android/tradefed/postprocessor/BasePostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/BasePostProcessor.java
@@ -242,7 +242,10 @@
                     continue;
                 }
                 // Force the metric to 'processed' since generated in a post-processor.
-                Metric newMetric = newEntry.getValue().setType(DataType.PROCESSED).build();
+                Metric newMetric =
+                        newEntry.getValue()
+                                .setType(getMetricType())
+                                .build();
                 testMetrics.put(newKey, newMetric);
             }
         } catch (RuntimeException e) {
@@ -325,8 +328,19 @@
                 continue;
             }
             // Force the metric to 'processed' since generated in a post-processor.
-            Metric newMetric = newEntry.getValue().setType(DataType.PROCESSED).build();
+            Metric newMetric =
+                    newEntry.getValue()
+                            .setType(getMetricType())
+                            .build();
             existing.put(newKey, newMetric);
         }
     }
+
+    /**
+     * Override this method to change the metric type if needed. By default metric is set to
+     * processed type.
+     */
+    protected DataType getMetricType() {
+        return DataType.PROCESSED;
+    }
 }
diff --git a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
index ab0033f..5628636 100644
--- a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
+++ b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
@@ -57,6 +57,9 @@
         try {
             CommandResult result = sandbox.run(config, listener);
             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+                CLog.e(
+                        "Sandbox finished with status: %s and exit code: %s",
+                        result.getStatus(), result.getExitCode());
                 handleStderrException(result.getStderr());
             }
         } finally {
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index f557b4b..5273668 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -99,6 +99,10 @@
                     + "including leading dash, e.g. \"-d\"")
     private Collection<String> mInstallArgs = new ArrayList<>();
 
+    @Option(name = "force-queryable",
+            description = "Whether apks should be installed as force queryable.")
+    private boolean mForceQueryable = true;
+
     @Option(
             name = "cleanup-apks",
             description =
@@ -265,6 +269,10 @@
             }
         }
 
+        if (mForceQueryable && getDevice().isAppEnumerationSupported()) {
+            mInstallArgs.add("--force-queryable");
+        }
+
         for (String testAppName : mTestFileNames) {
             installer(testInfo, Arrays.asList(new String[] {testAppName}));
         }
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
index 62c5875..31d31a7 100644
--- a/test_framework/Android.bp
+++ b/test_framework/Android.bp
@@ -20,6 +20,7 @@
     ],
     static_libs: [
         "longevity-host-lib",
+        "perfetto_metrics-full",
         "test-composers",
         "truth-prebuilt",
     ],
diff --git a/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java b/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
index 4f2e029..d86de45 100644
--- a/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
@@ -43,48 +43,48 @@
     @Option(name = "binary-stats-config", description = "Path to the binary Statsd config file")
     private File mBinaryConfig;
 
+    @Option(name = "per-run", description = "Collect Metrics at per-test level or per-run level")
+    private boolean mPerRun = true;
+
     /** Map from device serial number to config ID on that device */
     private Map<String, Long> mDeviceConfigIds = new HashMap<>();
 
+    /**
+     * Counting the test in the same run. It is used to distinguish the statsd result file from
+     * multiple tests
+     */
+    private int mTestCount = 1;
+
     @Override
     public void onTestRunStart(DeviceMetricData runData) {
-        mDeviceConfigIds.clear();
-        for (ITestDevice device : getDevices()) {
-            String serialNumber = device.getSerialNumber();
-            try {
-                long configId = pushBinaryStatsConfig(device, mBinaryConfig);
-                CLog.d(
-                        "Pushed binary stats config to device %s with config id: %d",
-                        serialNumber, configId);
-                mDeviceConfigIds.put(serialNumber, configId);
-            } catch (IOException | DeviceNotAvailableException e) {
-                CLog.e("Failed to push stats config to device %s, error: %s", serialNumber, e);
-            }
+        if (mPerRun) {
+            mDeviceConfigIds.clear();
+            startCollection();
+        }
+    }
+
+    @Override
+    public void onTestStart(DeviceMetricData testData) {
+        if (!mPerRun) {
+            mDeviceConfigIds.clear();
+            startCollection();
+        }
+    }
+
+    @Override
+    public void onTestEnd(
+            DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics) {
+        if (!mPerRun) {
+            stopCollection(testData);
+            mTestCount++;
         }
     }
 
     @Override
     public void onTestRunEnd(
             DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        for (ITestDevice device : getDevices()) {
-            String serialNumber = device.getSerialNumber();
-            if (mDeviceConfigIds.containsKey(serialNumber)) {
-                long configId = mDeviceConfigIds.get(serialNumber);
-                try {
-                    InputStreamSource dataStream = getReportByteStream(device, configId);
-                    CLog.d(
-                            "Retrieved stats report from device %s for config %d",
-                            serialNumber, configId);
-                    processStatsReport(device, dataStream, runData);
-                    testLog(
-                            String.format("device_%s_stats_report", serialNumber),
-                            LogDataType.PB,
-                            dataStream);
-                    removeConfig(device, configId);
-                } catch (DeviceNotAvailableException e) {
-                    CLog.e("Device %s not available: %s", serialNumber, e);
-                }
-            }
+        if (mPerRun) {
+            stopCollection(runData);
         }
     }
 
@@ -114,5 +114,47 @@
      * @param runData The destination where the processed metrics will be stored
      */
     protected void processStatsReport(
-            ITestDevice device, InputStreamSource dataStream, DeviceMetricData runData) {}
+            ITestDevice device, InputStreamSource dataStream, DeviceMetricData runData) {
+        // Empty method by default
+    }
+
+    private void startCollection() {
+        for (ITestDevice device : getDevices()) {
+            String serialNumber = device.getSerialNumber();
+            try {
+                long configId = pushBinaryStatsConfig(device, mBinaryConfig);
+                CLog.d(
+                        "Pushed binary stats config to device %s with config id: %d",
+                        serialNumber, configId);
+                mDeviceConfigIds.put(serialNumber, configId);
+            } catch (IOException | DeviceNotAvailableException e) {
+                CLog.e("Failed to push stats config to device %s, error: %s", serialNumber, e);
+            }
+        }
+    }
+
+    private void stopCollection(DeviceMetricData metricData) {
+        for (ITestDevice device : getDevices()) {
+            String serialNumber = device.getSerialNumber();
+            if (mDeviceConfigIds.containsKey(serialNumber)) {
+                long configId = mDeviceConfigIds.get(serialNumber);
+                try (InputStreamSource dataStream = getReportByteStream(device, configId)) {
+                    CLog.d(
+                            "Retrieved stats report from device %s for config %d",
+                            serialNumber, configId);
+                    removeConfig(device, configId);
+                    String reportName =
+                            mPerRun
+                                    ? String.format("device_%s_stats_report", serialNumber)
+                                    : String.format(
+                                            "device_%s_stats_report_test_%d",
+                                            serialNumber, mTestCount);
+                    testLog(reportName, LogDataType.PB, dataStream);
+                    processStatsReport(device, dataStream, metricData);
+                } catch (DeviceNotAvailableException e) {
+                    CLog.e("Device %s not available: %s", serialNumber, e);
+                }
+            }
+        }
+    }
 }
diff --git a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
new file mode 100644
index 0000000..5e0beb1
--- /dev/null
+++ b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2020 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.tradefed.postprocessor;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.LogFile;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.ZipUtil2;
+import com.android.tradefed.util.proto.TfMetricProtoUtil;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.TextFormat.ParseException;
+
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import perfetto.protos.PerfettoMergedMetrics.TraceMetrics;
+
+/**
+ * A post processor that processes text/binary metric perfetto proto file into key-value pairs by
+ * recursively expanding the proto messages and fields with string values until the field with
+ * numeric value is encountered. Treats enum and boolean as string values while constructing the
+ * keys.
+ *
+ * <p>It optionally supports indexing list fields when there are duplicates while constructing the
+ * keys. For example
+ *
+ * <p>"perfetto-indexed-list-field" - perfetto.protos.AndroidStartupMetric.Startup
+ *
+ * <p>android_startup-startup#1-package_name-com.calculator-to_first_frame-dur_ns: 300620342
+ * android_startup-startup#2-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
+ * android_startup-startup#3-package_name-com.calculator-to_first_frame-dur_ns: 261382005
+ */
+@OptionClass(alias = "perfetto-generic-processor")
+public class PerfettoGenericPostProcessor extends BasePostProcessor {
+
+    private static final String METRIC_SEP = "-";
+
+    public enum METRIC_FILE_FORMAT {
+        text,
+        binary,
+        json,
+    }
+
+    @Option(
+            name = "perfetto-proto-file-prefix",
+            description = "Prefix for identifying a perfetto metric file name.")
+    private Set<String> mPerfettoProtoMetricFilePrefix = new HashSet<>();
+
+    @Option(
+            name = "perfetto-indexed-list-field",
+            description = "List fields in perfetto proto metric file that has to be indexed.")
+    private Set<String> mPerfettoIndexedListFields = new HashSet<>();
+
+    @Option(
+            name = "perfetto-include-all-metrics",
+            description =
+                    "If this flag is turned on, all the metrics parsed from the perfetto file will"
+                            + " be included in the final result map and ignores the regex passed"
+                            + " in the filters.")
+    private boolean mPerfettoIncludeAllMetrics = false;
+
+    @Option(
+            name = "perfetto-metric-filter-regex",
+            description =
+                    "Regular expression that will be used for filtering the metrics parsed"
+                            + " from the perfetto proto metric file.")
+    private Set<String> mPerfettoMetricFilterRegEx = new HashSet<>();
+
+    @Option(
+            name = "trace-processor-output-format",
+            description = "Trace processor output format. One of [binary|text|json]")
+    private METRIC_FILE_FORMAT mTraceProcessorOutputFormat = METRIC_FILE_FORMAT.text;
+
+    @Option(
+            name = "decompress-perfetto-timeout",
+            description = "Timeout to decompress perfetto compressed file.",
+            isTimeVal = true)
+    private long mDecompressTimeoutMs = TimeUnit.MINUTES.toMillis(20);
+
+    @Option(
+            name = "processed-metric",
+            description =
+                    "True if the metric is final and shouldn't be processed any more,"
+                            + " false if the metric can be handled by another post-processor.")
+    private boolean mProcessedMetric = true;
+
+    // Matches 1.73, 1.73E+2
+    private Pattern mNumberWithExponentPattern =
+            Pattern.compile("[-+]?[0-9]*[\\.]?[0-9]+([eE][-+]?[0-9]+)?");
+
+    // Matches numbers without exponent format.
+    private Pattern mNumberPattern = Pattern.compile("[-+]?[0-9]*[\\.]?[0-9]+");
+
+    private List<Pattern> mMetricPatterns = new ArrayList<>();
+
+
+    @Override
+    public Map<String, Metric.Builder> processTestMetricsAndLogs(
+            TestDescription testDescription,
+            HashMap<String, Metric> testMetrics,
+            Map<String, LogFile> testLogs) {
+        buildMetricFilterPatterns();
+        return processPerfettoMetrics(filterPerfeticMetricFiles(testLogs));
+    }
+
+    @Override
+    public Map<String, Metric.Builder> processRunMetricsAndLogs(
+            HashMap<String, Metric> rawMetrics, Map<String, LogFile> runLogs) {
+        buildMetricFilterPatterns();
+        return processPerfettoMetrics(filterPerfeticMetricFiles(runLogs));
+    }
+
+    /**
+     * Filter the perfetto metric file based on the prefix.
+     *
+     * @param logs
+     * @return files matched the prefix.
+     */
+    private List<File> filterPerfeticMetricFiles(Map<String, LogFile> logs) {
+        List<File> perfettoMetricFiles = new ArrayList<>();
+        for (String key : logs.keySet()) {
+            Optional<String> reportPrefix =
+                    mPerfettoProtoMetricFilePrefix
+                            .stream()
+                            .filter(prefix -> key.startsWith(prefix))
+                            .findAny();
+
+            if (!reportPrefix.isPresent()) {
+                continue;
+            }
+            perfettoMetricFiles.add(new File(logs.get(key).getPath()));
+        }
+        return perfettoMetricFiles;
+    }
+
+    /**
+     * Process perfetto metric files into key, value pairs.
+     *
+     * @param perfettoMetricFiles perfetto metric files to be processed.
+     * @return key, value pairs processed from the metrics.
+     */
+    private Map<String, Metric.Builder> processPerfettoMetrics(List<File> perfettoMetricFiles) {
+        Map<String, Metric.Builder> parsedMetrics = new HashMap<>();
+        File uncompressedDir = null;
+        for (File perfettoMetricFile : perfettoMetricFiles) {
+            // Text files by default are compressed before uploading. Decompress the text proto
+            // file before post processing.
+            try {
+                if (!(mTraceProcessorOutputFormat == METRIC_FILE_FORMAT.binary) &&
+                        ZipUtil.isZipFileValid(perfettoMetricFile, true)) {
+                    ZipFile perfettoZippedFile = new ZipFile(perfettoMetricFile);
+                    uncompressedDir = FileUtil.createTempDir("uncompressed_perfetto_metric");
+                    ZipUtil2.extractZip(perfettoZippedFile, uncompressedDir);
+                    perfettoMetricFile = uncompressedDir.listFiles()[0];
+                    perfettoZippedFile.close();
+                }
+            } catch (IOException e) {
+                CLog.e(
+                        "IOException happened when unzipping the perfetto metric proto"
+                                + " file."
+                                + e.getMessage());
+            }
+
+            // Parse the perfetto proto file.
+            try (BufferedReader bufferedReader = new BufferedReader(
+                    new FileReader(perfettoMetricFile))) {
+                switch (mTraceProcessorOutputFormat) {
+                    case text:
+                        TraceMetrics.Builder builder = TraceMetrics.newBuilder();
+                        TextFormat.merge(bufferedReader, builder);
+                        parsedMetrics.putAll(
+                                filterMetrics(convertPerfettoProtoMessage(builder.build())));
+                        break;
+                    case binary:
+                        TraceMetrics metricProto = null;
+                        metricProto = TraceMetrics
+                                .parseFrom(new FileInputStream(perfettoMetricFile));
+                        parsedMetrics
+                                .putAll(filterMetrics(convertPerfettoProtoMessage(metricProto)));
+                        break;
+                    case json:
+                        CLog.w("JSON perfetto metric file processing not supported.");
+                }
+            } catch (ParseException e) {
+                CLog.e("Failed to merge the perfetto metric file. " + e.getMessage());
+            } catch (IOException ioe) {
+                CLog.e(
+                        "IOException happened when reading the perfetto metric file. "
+                                + ioe.getMessage());
+            } finally {
+                // Delete the uncompressed perfetto metric proto file directory.
+                FileUtil.recursiveDelete(uncompressedDir);
+            }
+        }
+        return parsedMetrics;
+    }
+
+    /**
+     * Expands the metric proto file as tree structure and converts it into key, value pairs by
+     * recursively constructing the key using the message name, proto fields with string values
+     * until the numeric proto field is encountered.
+     *
+     * <p>android_startup-startup-package_name-com.calculator-to_first_frame-dur_ns: 300620342
+     * android_startup-startup-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
+     *
+     * <p>It also supports indexing the list proto fields optionally. This will be used if the list
+     * generates duplicate key's when recursively expanding the messages to prevent overriding the
+     * results.
+     *
+     * <p>"perfetto-indexed-list-field" - perfetto.protos.AndroidStartupMetric.Startup
+     *
+     * <p><android_startup-startup#1-package_name-com.calculator-to_first_frame-dur_ns: 300620342
+     * android_startup-startup#2-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
+     * android_startup-startup#3-package_name-com.calculator-to_first_frame-dur_ns: 261382005
+     */
+    private Map<String, Metric.Builder> convertPerfettoProtoMessage(Message reportMessage) {
+        Map<FieldDescriptor, Object> fields = reportMessage.getAllFields();
+        Map<String, Metric.Builder> convertedMetrics = new HashMap<String, Metric.Builder>();
+        List<String> keyPrefixes = new ArrayList<String>();
+
+        for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
+            if (!(entry.getValue() instanceof Message) && !(entry.getValue() instanceof List)) {
+                if (isNumeric(entry.getValue().toString())) {
+                    // Construct the metric if it is numeric value.
+                    if (mNumberPattern.matcher(entry.getValue().toString()).matches()) {
+                        convertedMetrics.put(
+                                entry.getKey().getName(),
+                                TfMetricProtoUtil.stringToMetric(entry.getValue().toString())
+                                        .toBuilder());
+                    } else {
+                        // Parse the exponent notation of string before adding it to metric.
+                        convertedMetrics.put(
+                                entry.getKey().getName(),
+                                TfMetricProtoUtil.stringToMetric(
+                                                Long.toString(
+                                                        Double.valueOf(entry.getValue().toString())
+                                                                .longValue()))
+                                        .toBuilder());
+                    }
+                } else {
+                    // Add to prefix list if string value is encountered.
+                    keyPrefixes.add(
+                            String.join(
+                                    METRIC_SEP,
+                                    entry.getKey().getName().toString(),
+                                    entry.getValue().toString()));
+                }
+            }
+        }
+
+        // Recursively expand the proto messages and repeated fields(i.e list).
+        // Recursion when there are no messages or list with in the current message.
+        for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
+            if (entry.getValue() instanceof Message) {
+                Map<String, Metric.Builder> messageMetrics =
+                        convertPerfettoProtoMessage((Message) entry.getValue());
+                for (Entry<String, Metric.Builder> metricEntry : messageMetrics.entrySet()) {
+                    // Add prefix to the metrics parsed from this message.
+                    for (String prefix : keyPrefixes) {
+                        convertedMetrics.put(
+                                String.join(
+                                        METRIC_SEP,
+                                        prefix,
+                                        entry.getKey().getName(),
+                                        metricEntry.getKey()),
+                                metricEntry.getValue());
+                    }
+                    if (keyPrefixes.isEmpty()) {
+                        convertedMetrics.put(
+                                String.join(
+                                        METRIC_SEP, entry.getKey().getName(), metricEntry.getKey()),
+                                metricEntry.getValue());
+                    }
+                }
+            } else if (entry.getValue() instanceof List) {
+                List<? extends Object> listMetrics = (List) entry.getValue();
+                for (int i = 0; i < listMetrics.size(); i++) {
+                    String metricKeyRoot;
+                    // Use indexing if the current field is chosen for indexing.
+                    // Use it if metrics keys generated has duplicates to prevent overriding.
+                    if (mPerfettoIndexedListFields.contains(entry.getKey().toString())) {
+                        metricKeyRoot =
+                                String.join(
+                                        METRIC_SEP,
+                                        entry.getKey().getName(),
+                                        String.valueOf(i + 1));
+                    } else {
+                        metricKeyRoot = String.join(METRIC_SEP, entry.getKey().getName());
+                    }
+                    if (listMetrics.get(i) instanceof Message) {
+                        Map<String, Metric.Builder> messageMetrics =
+                                convertPerfettoProtoMessage((Message) listMetrics.get(i));
+                        for (Entry<String, Metric.Builder> metricEntry :
+                                messageMetrics.entrySet()) {
+                            for (String prefix : keyPrefixes) {
+                                convertedMetrics.put(
+                                        String.join(
+                                                METRIC_SEP,
+                                                prefix,
+                                                metricKeyRoot,
+                                                metricEntry.getKey()),
+                                        metricEntry.getValue());
+                            }
+                            if (keyPrefixes.isEmpty()) {
+                                convertedMetrics.put(
+                                        String.join(
+                                                METRIC_SEP, metricKeyRoot, metricEntry.getKey()),
+                                        metricEntry.getValue());
+                            }
+                        }
+                    } else {
+                        convertedMetrics.put(
+                                metricKeyRoot,
+                                TfMetricProtoUtil.stringToMetric(listMetrics.get(i).toString())
+                                        .toBuilder());
+                    }
+                }
+            }
+        }
+        return convertedMetrics;
+    }
+
+    /**
+     * Check if the given string is number. It matches the string with exponent notation as well.
+     *
+     * <p>For example returns true for Return true for 1.73, 1.73E+2
+     */
+    private boolean isNumeric(String strNum) {
+        if (strNum == null) {
+            return false;
+        }
+        return mNumberWithExponentPattern.matcher(strNum).matches();
+    }
+
+    /** Build regular expression patterns to filter the metrics. */
+    private void buildMetricFilterPatterns() {
+        if (!mPerfettoMetricFilterRegEx.isEmpty() && mMetricPatterns.isEmpty()) {
+            for (String regEx : mPerfettoMetricFilterRegEx) {
+                mMetricPatterns.add(Pattern.compile(regEx));
+            }
+        }
+    }
+
+    /**
+     * Filter parsed metrics from the proto metric files based on the regular expression. If
+     * "mPerfettoIncludeAllMetrics" is enabled then filters will be ignored and returns all the
+     * parsed metrics.
+     */
+    private Map<String, Metric.Builder> filterMetrics(Map<String, Metric.Builder> parsedMetrics) {
+        if (mPerfettoIncludeAllMetrics) {
+            return parsedMetrics;
+        }
+        Map<String, Metric.Builder> filteredMetrics = new HashMap<>();
+        for (Entry<String, Metric.Builder> metricEntry : parsedMetrics.entrySet()) {
+            for (Pattern pattern : mMetricPatterns) {
+                if (pattern.matcher(metricEntry.getKey()).matches()) {
+                    filteredMetrics.put(metricEntry.getKey(), metricEntry.getValue());
+                    break;
+                }
+            }
+        }
+        return filteredMetrics;
+    }
+
+    /**
+     * Set the metric type based on flag.
+     */
+    @Override
+    protected DataType getMetricType() {
+        return mProcessedMetric ? DataType.PROCESSED : DataType.RAW;
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java b/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
index ca5ce91..1e4429c 100644
--- a/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
+++ b/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
@@ -51,6 +51,10 @@
                     + "including leading dash, e.g. \"-d\"")
     private Collection<String> mInstallArgs = new ArrayList<>();
 
+    @Option(name = "force-queryable",
+            description = "Whether apks should be installed as force queryable.")
+    private boolean mForceQueryable = true;
+
     @Option(name = "cleanup-apks",
             description = "Whether apks installed should be uninstalled after test. Note that the "
                     + "preparer does not verify if the apks are successfully removed.")
@@ -81,6 +85,9 @@
             throw new TargetSetupError("Failed to find a valid test zip directory.",
                     device.getDeviceDescriptor());
         }
+        if (mForceQueryable && device.isAppEnumerationSupported()) {
+            mInstallArgs.add("--force-queryable");
+        }
         resolveAbi(device);
         installApksRecursively(testsDir, device);
     }
diff --git a/test_framework/com/android/tradefed/targetprep/AppSetup.java b/test_framework/com/android/tradefed/targetprep/AppSetup.java
index 62a8464..8a7a573 100644
--- a/test_framework/com/android/tradefed/targetprep/AppSetup.java
+++ b/test_framework/com/android/tradefed/targetprep/AppSetup.java
@@ -63,6 +63,10 @@
     @Option(name = "install-arg", description = "optional flag(s) to provide when installing apks.")
     private ArrayList<String> mInstallArgs = new ArrayList<>();
 
+    @Option(name = "force-queryable",
+            description = "Whether apks should be installed as force queryable.")
+    private boolean mForceQueryable = true;
+
     @Option(name = "post-install-cmd", description =
             "optional post-install adb shell commands; can be repeated.")
     private List<String> mPostInstallCmds = new ArrayList<>();
@@ -102,6 +106,10 @@
         }
 
         if (mInstall) {
+            if (mForceQueryable && device.isAppEnumerationSupported()) {
+                mInstallArgs.add("--force-queryable");
+            }
+
             for (VersionedFile apkFile : apps) {
                 if (mCheckMinSdk) {
                     AaptParser aaptParser = doAaptParse(apkFile.getFile());
diff --git a/test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java b/test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java
index 62df086..51baa0f 100644
--- a/test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java
@@ -30,13 +30,33 @@
  */
 @OptionClass(alias = "fastboot-command-preparer")
 public class FastbootCommandPreparer extends BaseTargetPreparer {
+
+    private enum FastbootMode {
+        BOOTLOADER,
+        FASTBOOTD,
+    }
+
+    @Option(
+            name = "fastboot-mode",
+            description = "True to boot the device into bootloader mode, false for fastbootd mode.")
+    private FastbootMode mFastbootMode = FastbootMode.BOOTLOADER;
+
+    @Option(
+            name = "stay-fastboot",
+            description = "True to keep the device in bootloader or fastbootd mode.")
+    private boolean mStayFastboot = false;
+
     @Option(name = "command", description = "Fastboot commands to run.")
     private List<String> mFastbootCommands = new ArrayList<String>();
 
     @Override
     public void setUp(TestInformation testInformation)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        testInformation.getDevice().rebootIntoBootloader();
+        if (mFastbootMode == FastbootMode.BOOTLOADER) {
+            testInformation.getDevice().rebootIntoBootloader();
+        } else {
+            testInformation.getDevice().rebootIntoFastbootd();
+        }
 
         for (String cmd : mFastbootCommands) {
             // Ignore reboots since we'll reboot in the end.
@@ -47,7 +67,9 @@
             testInformation.getDevice().executeFastbootCommand(cmd.split("\\s+"));
         }
 
-        testInformation.getDevice().reboot();
+        if (!mStayFastboot) {
+            testInformation.getDevice().reboot();
+        }
     }
 
     /** {@inheritDoc} */
diff --git a/test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java b/test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
index 0dd9b97..4ad51e2 100644
--- a/test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
+++ b/test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
@@ -48,6 +48,10 @@
     )
     private Collection<String> mInstallArgs = new ArrayList<>();
 
+    @Option(name = "force-queryable",
+            description = "Whether apks should be installed as force queryable.")
+    private boolean mForceQueryable = true;
+
     @Option(
         name = "cleanup-apks",
         description =
@@ -103,6 +107,10 @@
                     "Failed to extract test zip.", e, device.getDeviceDescriptor());
         }
 
+        if (mForceQueryable && device.isAppEnumerationSupported()) {
+            mInstallArgs.add("--force-queryable");
+        }
+
         try {
             installApksRecursively(testsDir, device);
         } finally {
diff --git a/test_framework/com/android/tradefed/targetprep/InstallApkSetup.java b/test_framework/com/android/tradefed/targetprep/InstallApkSetup.java
index 36f4053..651a657 100644
--- a/test_framework/com/android/tradefed/targetprep/InstallApkSetup.java
+++ b/test_framework/com/android/tradefed/targetprep/InstallApkSetup.java
@@ -63,6 +63,10 @@
                     + "including leading dash, e.g. \"-d\"")
     private Collection<String> mInstallArgs = new ArrayList<>();
 
+    @Option(name = "force-queryable",
+            description = "Whether apks should be installed as force queryable.")
+    private boolean mForceQueryable = true;
+
     @Option(name = "post-install-cmd", description =
             "optional post-install adb shell commands; can be repeated.")
     private List<String> mPostInstallCmds = new ArrayList<>();
@@ -95,6 +99,10 @@
                     mInstallArgs.add(String.format("--abi %s", abi));
                 }
             }
+            if (mForceQueryable && device.isAppEnumerationSupported()
+                    && !mInstallArgs.contains("--force-queryable")) {
+                mInstallArgs.add("--force-queryable");
+            }
             String result = device.installPackage(apk, true, mInstallArgs.toArray(new String[]{}));
             if (result != null) {
                 if (mThrowIfInstallFail) {
diff --git a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index 6190f3c..3afabc6 100644
--- a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
@@ -257,13 +258,12 @@
         return src;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError,
-            DeviceNotAvailableException {
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
         mFilesPushed = new HashSet<>();
+        ITestDevice device = testInfo.getDevice();
         if (mRemountSystem) {
             device.remountSystemWritable();
         }
@@ -292,7 +292,7 @@
                     String.format(
                             "Trying to push local '%s' to remote '%s'",
                             local.getPath(), remotePath));
-            evaluatePushingPair(device, buildInfo, local, remotePath);
+            evaluatePushingPair(device, testInfo.getBuildInfo(), local, remotePath);
         }
 
         for (String command : mPostPushCommands) {
@@ -305,12 +305,10 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
-            throws DeviceNotAvailableException {
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        ITestDevice device = testInfo.getDevice();
         if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) {
             if (mRemountSystem) {
                 device.remountSystemWritable();
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index c299b28..19dc753 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -135,6 +135,7 @@
 import com.android.tradefed.invoker.TestInvocationTest;
 import com.android.tradefed.invoker.UnexecutedTestReporterThreadTest;
 import com.android.tradefed.invoker.logger.InvocationMetricLoggerTest;
+import com.android.tradefed.invoker.logger.TfObjectTrackerTest;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecutionTest;
 import com.android.tradefed.invoker.shard.ShardHelperTest;
 import com.android.tradefed.invoker.shard.StrictShardHelperTest;
@@ -151,6 +152,7 @@
 import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
 import com.android.tradefed.postprocessor.AveragePostProcessorTest;
 import com.android.tradefed.postprocessor.BasePostProcessorTest;
+import com.android.tradefed.postprocessor.PerfettoGenericPostProcessorTest;
 import com.android.tradefed.postprocessor.StatsdEventMetricPostProcessorTest;
 import com.android.tradefed.postprocessor.StatsdGenericPostProcessorTest;
 import com.android.tradefed.result.ATestFileSystemLogSaverTest;
@@ -552,6 +554,7 @@
 
     // invoker.logger
     InvocationMetricLoggerTest.class,
+    TfObjectTrackerTest.class,
 
     // invoker.shard
     ShardHelperTest.class,
@@ -580,6 +583,7 @@
     AggregatePostProcessorTest.class,
     AveragePostProcessorTest.class,
     BasePostProcessorTest.class,
+    PerfettoGenericPostProcessorTest.class,
     StatsdEventMetricPostProcessorTest.class,
     StatsdGenericPostProcessorTest.class,
 
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
index cfb080d..b129c37 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
@@ -26,6 +26,8 @@
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.cluster.ClusterCommand.RequestType;
+import com.android.tradefed.cluster.ClusterCommandScheduler.InvocationEventHandler;
 import com.android.tradefed.command.CommandScheduler;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.Configuration;
@@ -45,6 +47,7 @@
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ConsoleResultReporter;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -56,15 +59,13 @@
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRestApiHelper;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.ZipUtil;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 import com.android.tradefed.util.keystore.StubKeyStoreClient;
 
-import com.android.tradefed.cluster.ClusterCommand.RequestType;
-import com.android.tradefed.cluster.ClusterCommandScheduler.InvocationEventHandler;
-import com.android.tradefed.util.IRestApiHelper;
 import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponse;
@@ -87,7 +88,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.junit.Test;
 import org.mockito.Mockito;
 
 import java.io.ByteArrayInputStream;
@@ -99,11 +99,11 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.UUID;
 import java.util.Map;
 import java.util.Set;
 import java.util.Stack;
 import java.util.TreeMap;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link ClusterCommandScheduler}. */
@@ -138,7 +138,7 @@
 
     // Explicitly define this, so we can mock it
     private static interface ICommandEventUploader
-            extends IClusterEventUploader<ClusterCommandEvent> {};
+            extends IClusterEventUploader<ClusterCommandEvent> {}
 
     @Before
     public void setUp() throws Exception {
@@ -651,8 +651,8 @@
         handler.testRunEnded(10L, new HashMap<String, Metric>());
         handler.invocationEnded(100L);
         context.addAllocatedDevice(DEVICE_SERIAL, mockTestDevice);
-        context.addInvocationTimingMetric(IInvocationContext.TimingEvent.FETCH_BUILD, 100L);
-        context.addInvocationTimingMetric(IInvocationContext.TimingEvent.SETUP, 200L);
+        context.addInvocationAttribute(InvocationMetricKey.FETCH_BUILD.toString(), "100");
+        context.addInvocationAttribute(InvocationMetricKey.SETUP.toString(), "200");
         Map<ITestDevice, FreeDeviceState> releaseMap = new HashMap<>();
         releaseMap.put(mockTestDevice, FreeDeviceState.AVAILABLE);
         handler.invocationComplete(context, releaseMap);
diff --git a/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
index 28335ef..c123833 100644
--- a/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
@@ -41,6 +41,7 @@
 import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
 import org.easymock.Capture;
 import org.easymock.EasyMock;
+import org.easymock.IAnswer;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -61,22 +62,27 @@
             super(device, stateMonitor, allocationMonitor);
         }
 
-        IDevice currentDevice = null;
-        IRunUtil currentRunUtil = null;
+        IRunUtil currentRunUtil;
+        boolean expectToConnect;
 
         @Override
-        public IDevice getIDevice() {
-            return currentDevice;
+        public boolean adbTcpConnect(String host, String port) {
+            Assert.assertTrue("Unexpected method call to adbTcpConnect.", expectToConnect);
+            Assert.assertEquals(IP_ADDRESS, host);
+            Assert.assertEquals(PORT, port);
+            return true;
         }
 
         @Override
-        public void setIDevice(IDevice device) {
-            currentDevice = device;
+        public boolean adbTcpDisconnect(String host, String port) {
+            Assert.assertEquals(IP_ADDRESS, host);
+            Assert.assertEquals(PORT, port);
+            return true;
         }
 
         @Override
-        public void setDeviceState(TestDeviceState state) {
-            Assert.assertEquals(TestDeviceState.NOT_AVAILABLE, state);
+        public void waitForDeviceAvailable() {
+            Assert.assertTrue("Unexpected method call to waitForDeviceAvailable.", expectToConnect);
         }
 
         @Override
@@ -94,9 +100,44 @@
     }
 
     private static final String STUB_SERIAL_NUMBER = "local-virtual-device-0";
-    private static final String ONLINE_SERIAL_NUMBER = "127.0.0.1:6520";
+    private static final String IP_ADDRESS = "127.0.0.1";
+    private static final String PORT = "6520";
+    private static final String ONLINE_SERIAL_NUMBER = IP_ADDRESS + ":" + PORT;
+    private static final String INSTANCE_NAME = "local-instance-1";
     private static final String BUILD_FLAVOR = "cf_x86_phone-userdebug";
     private static final long ACLOUD_TIMEOUT = 12345;
+    private static final String SUCCESS_REPORT_STRING =
+            String.format(
+                    "{"
+                            + " \"command\": \"create\","
+                            + " \"data\": {"
+                            + "  \"devices\": ["
+                            + "   {"
+                            + "    \"ip\": \"%s\","
+                            + "    \"instance_name\": \"%s\""
+                            + "   }"
+                            + "  ]"
+                            + " },"
+                            + " \"errors\": [],"
+                            + " \"status\": \"SUCCESS\""
+                            + "}",
+                    ONLINE_SERIAL_NUMBER, INSTANCE_NAME);
+    private static final String FAILURE_REPORT_STRING =
+            String.format(
+                    "{"
+                            + " \"command\": \"create\","
+                            + " \"data\": {"
+                            + "  \"devices_failing_boot\": ["
+                            + "   {"
+                            + "    \"ip\": \"%s\","
+                            + "    \"instance_name\": \"%s\""
+                            + "   }"
+                            + "  ]"
+                            + " },"
+                            + " \"errors\": [],"
+                            + " \"status\": \"BOOT_FAIL\""
+                            + "}",
+                    ONLINE_SERIAL_NUMBER, INSTANCE_NAME);
 
     // Temporary files.
     private File mAcloud;
@@ -104,12 +145,7 @@
     private File mHostPackageTarGzip;
     private File mTmpDir;
 
-    // The initial stub device.
-    private StubLocalAndroidVirtualDevice mStubLocalAvd;
-
-    // Mock objects
-    private IDeviceStateMonitor mMockDeviceStateMonitor;
-    private IDeviceMonitor mMockDeviceMonitor;
+    // Mock object.
     private IDeviceBuildInfo mMockDeviceBuildInfo;
 
     // The object under test.
@@ -123,20 +159,22 @@
         createHostPackage(mHostPackageTarGzip);
         mTmpDir = FileUtil.createTempDir("LocalAvdTmp");
 
-        mStubLocalAvd = new StubLocalAndroidVirtualDevice(STUB_SERIAL_NUMBER);
-
-        mMockDeviceStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
-        mMockDeviceMonitor = EasyMock.createMock(IDeviceMonitor.class);
         mMockDeviceBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
         EasyMock.expect(mMockDeviceBuildInfo.getDeviceImageFile()).andReturn(mImageZip);
         EasyMock.expect(mMockDeviceBuildInfo.getFile(EasyMock.eq("cvd-host_package.tar.gz")))
                 .andReturn(mHostPackageTarGzip);
         EasyMock.expect(mMockDeviceBuildInfo.getBuildFlavor()).andReturn(BUILD_FLAVOR);
+        IDeviceStateMonitor mockDeviceStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
+        mockDeviceStateMonitor.setIDevice(EasyMock.anyObject());
+        EasyMock.expectLastCall().anyTimes();
+        IDeviceMonitor mockDeviceMonitor = EasyMock.createMock(IDeviceMonitor.class);
+        EasyMock.replay(mMockDeviceBuildInfo, mockDeviceStateMonitor, mockDeviceMonitor);
 
         mLocalAvd =
                 new TestableLocalAndroidVirtualDevice(
-                        mStubLocalAvd, mMockDeviceStateMonitor, mMockDeviceMonitor);
-        mLocalAvd.setIDevice(mStubLocalAvd);
+                        new StubLocalAndroidVirtualDevice(STUB_SERIAL_NUMBER),
+                        mockDeviceStateMonitor,
+                        mockDeviceMonitor);
         TestDeviceOptions options = mLocalAvd.getOptions();
         options.setGceCmdTimeout(ACLOUD_TIMEOUT);
         options.setAvdDriverBinary(mAcloud);
@@ -178,35 +216,54 @@
         }
     }
 
-    private void replayAllMocks(Object... mocks) {
-        EasyMock.replay(mocks);
-        EasyMock.replay(mMockDeviceStateMonitor, mMockDeviceMonitor, mMockDeviceBuildInfo);
-    }
-
     private IRunUtil mockAcloudCreate(
-            CommandStatus status, Capture<String> hostPackageDir, Capture<String> imageDir) {
+            CommandStatus status,
+            String reportString,
+            Capture<String> reportFile,
+            Capture<String> hostPackageDir,
+            Capture<String> imageDir) {
         IRunUtil runUtil = EasyMock.createMock(IRunUtil.class);
         runUtil.setEnvVariable(EasyMock.eq("TMPDIR"), EasyMock.eq(mTmpDir.getAbsolutePath()));
-        runUtil.setEnvVariable(EasyMock.eq("ANDROID_HOST_OUT"), EasyMock.capture(hostPackageDir));
         runUtil.setEnvVariable(EasyMock.eq("TARGET_PRODUCT"), EasyMock.eq(BUILD_FLAVOR));
 
-        CommandResult result = new CommandResult(status);
-        result.setStderr("acloud create");
-        result.setStdout("acloud create");
+        IAnswer<CommandResult> writeToReportFile =
+                new IAnswer() {
+                    @Override
+                    public CommandResult answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        for (int index = 0; index < args.length; index++) {
+                            if ("--report_file".equals(args[index])) {
+                                index++;
+                                File file = new File((String) args[index]);
+                                FileUtil.writeToFile(reportString, file);
+                            }
+                        }
+
+                        CommandResult result = new CommandResult(status);
+                        result.setStderr("acloud create");
+                        result.setStdout("acloud create");
+                        return result;
+                    }
+                };
+
         EasyMock.expect(
                         runUtil.runTimedCmd(
                                 EasyMock.eq(ACLOUD_TIMEOUT),
-                                EasyMock.startsWith(mAcloud.getAbsolutePath()),
+                                EasyMock.eq(mAcloud.getAbsolutePath()),
                                 EasyMock.eq("create"),
                                 EasyMock.eq("--local-instance"),
-                                EasyMock.eq("1"),
                                 EasyMock.eq("--local-image"),
                                 EasyMock.capture(imageDir),
+                                EasyMock.eq("--local-tool"),
+                                EasyMock.capture(hostPackageDir),
+                                EasyMock.eq("--report_file"),
+                                EasyMock.capture(reportFile),
+                                EasyMock.eq("--no-autoconnect"),
                                 EasyMock.eq("--yes"),
                                 EasyMock.eq("--skip-pre-run-check"),
                                 EasyMock.eq("-vv"),
                                 EasyMock.eq("-test")))
-                .andReturn(result);
+                .andAnswer(writeToReportFile);
 
         return runUtil;
     }
@@ -221,10 +278,11 @@
         EasyMock.expect(
                         runUtil.runTimedCmd(
                                 EasyMock.eq(ACLOUD_TIMEOUT),
-                                EasyMock.startsWith(mAcloud.getAbsolutePath()),
+                                EasyMock.eq(mAcloud.getAbsolutePath()),
                                 EasyMock.eq("delete"),
+                                EasyMock.eq("--local-only"),
                                 EasyMock.eq("--instance-names"),
-                                EasyMock.eq("local-instance-1"),
+                                EasyMock.eq(INSTANCE_NAME),
                                 EasyMock.eq("-vv")))
                 .andReturn(result);
 
@@ -269,10 +327,16 @@
     @Test
     public void testPreinvocationSetupSuccess()
             throws DeviceNotAvailableException, IOException, TargetSetupError {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.SUCCESS, hostPackageDir, imageDir);
+                mockAcloudCreate(
+                        CommandStatus.SUCCESS,
+                        SUCCESS_REPORT_STRING,
+                        reportFile,
+                        hostPackageDir,
+                        imageDir);
 
         IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.SUCCESS);
 
@@ -281,11 +345,12 @@
         IDevice mockOnlineDevice = EasyMock.createMock(IDevice.class);
         EasyMock.expect(mockOnlineDevice.getSerialNumber()).andReturn(ONLINE_SERIAL_NUMBER);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger, mockOnlineDevice);
+        EasyMock.replay(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger, mockOnlineDevice);
 
         // Test setUp.
         mLocalAvd.setTestLogger(testLogger);
         mLocalAvd.currentRunUtil = acloudCreateRunUtil;
+        mLocalAvd.expectToConnect = true;
         mLocalAvd.preInvocationSetup(mMockDeviceBuildInfo, null);
 
         Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
@@ -300,34 +365,42 @@
         // Create the logs and configuration that the local AVD object expects.
         File runtimeDir =
                 FileUtil.getFileForPath(
-                        mTmpDir, "acloud_cvd_temp", "instance_home_1", "cuttlefish_runtime");
+                        mTmpDir, "acloud_cvd_temp", INSTANCE_NAME, "cuttlefish_runtime");
         Assert.assertTrue(runtimeDir.mkdirs());
         createEmptyFiles(
                 runtimeDir, "kernel.log", "logcat", "launcher.log", "cuttlefish_config.json");
 
         // Test tearDown.
         mLocalAvd.currentRunUtil = acloudDeleteRunUtil;
+        mLocalAvd.expectToConnect = false;
         mLocalAvd.postInvocationTearDown(null);
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
 
-    /** Test that the device cannot boot within timeout. */
+    /** Test that the acloud command reports failure. */
     @Test
-    public void testPreInvocationSetupTimeout() throws DeviceNotAvailableException {
+    public void testPreInvocationSetupBootFailure() throws DeviceNotAvailableException {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.TIMED_OUT, hostPackageDir, imageDir);
+                mockAcloudCreate(
+                        CommandStatus.SUCCESS,
+                        FAILURE_REPORT_STRING,
+                        reportFile,
+                        hostPackageDir,
+                        imageDir);
 
         IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.FAILED);
 
         ITestLogger testLogger = EasyMock.createMock(ITestLogger.class);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
+        EasyMock.replay(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
 
         // Test setUp.
         TargetSetupError expectedException = null;
@@ -340,7 +413,7 @@
             expectedException = e;
         }
 
-        Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
+        Assert.assertEquals(STUB_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
 
         File capturedHostPackageDir = new File(hostPackageDir.getValue());
         File capturedImageDir = new File(imageDir.getValue());
@@ -353,23 +426,23 @@
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
 
-    /** Test that the device fails to boot. */
+    /** Test that the acloud command fails, and the report is empty. */
     @Test
     public void testPreInvocationSetupFailure() throws DeviceNotAvailableException {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.FAILED, hostPackageDir, imageDir);
-
-        IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.FAILED);
+                mockAcloudCreate(CommandStatus.FAILED, "", reportFile, hostPackageDir, imageDir);
 
         ITestLogger testLogger = EasyMock.createMock(ITestLogger.class);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
+        EasyMock.replay(acloudCreateRunUtil, testLogger);
 
         // Test setUp.
         TargetSetupError expectedException = null;
@@ -382,7 +455,7 @@
             expectedException = e;
         }
 
-        Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
+        Assert.assertEquals(STUB_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
 
         File capturedHostPackageDir = new File(hostPackageDir.getValue());
         File capturedImageDir = new File(imageDir.getValue());
@@ -390,11 +463,12 @@
         Assert.assertTrue(capturedImageDir.isDirectory());
 
         // Test tearDown.
-        mLocalAvd.currentRunUtil = acloudDeleteRunUtil;
+        mLocalAvd.currentRunUtil = null;
         mLocalAvd.postInvocationTearDown(expectedException);
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index c45e163..60e316b 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -1666,6 +1666,106 @@
         EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
     }
 
+    /** Unit test for {@link NativeDevice#rebootIntoBootloader()}}. */
+    @Test
+    public void testRebootIntoBootloader() throws Exception {
+        NativeDevice testDevice =
+                new NativeDevice(mMockIDevice, mMockStateMonitor, mMockDvcMonitor) {
+                    @Override
+                    public TestDeviceState getDeviceState() {
+                        return TestDeviceState.ONLINE;
+                    }
+                };
+        String into = "bootloader";
+        mMockIDevice.reboot(into);
+        EasyMock.expectLastCall();
+        EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
+                .andReturn(true);
+        EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+        testDevice.rebootIntoBootloader();
+        EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+    }
+
+    /**
+     * Unit test for {@link NativeDevice#rebootIntoBootloader()}} when device is already in fastboot
+     * mode.
+     */
+    @Test
+    public void testRebootIntoBootloader_forceFastboot() throws Exception {
+        TestableAndroidNativeDevice testDevice =
+                new TestableAndroidNativeDevice() {
+                    @Override
+                    public TestDeviceState getDeviceState() {
+                        return TestDeviceState.FASTBOOT;
+                    }
+
+                    @Override
+                    public CommandResult executeFastbootCommand(String... cmdArgs)
+                            throws DeviceNotAvailableException, UnsupportedOperationException {
+                        if (cmdArgs[0].equals("reboot-bootloader")) {
+                            wasCalled = true;
+                        }
+                        return new CommandResult();
+                    }
+                };
+        EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
+                .andReturn(true);
+        EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+        testDevice.rebootIntoBootloader();
+        assertTrue(testDevice.wasCalled);
+        EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+    }
+
+    /** Unit test for {@link NativeDevice#rebootIntoFastbootd()}}. */
+    @Test
+    public void testRebootIntoFastbootd() throws Exception {
+        NativeDevice testDevice =
+                new NativeDevice(mMockIDevice, mMockStateMonitor, mMockDvcMonitor) {
+                    @Override
+                    public TestDeviceState getDeviceState() {
+                        return TestDeviceState.ONLINE;
+                    }
+                };
+        String into = "fastboot";
+        mMockIDevice.reboot(into);
+        EasyMock.expectLastCall();
+        EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
+                .andReturn(true);
+        EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+        testDevice.rebootIntoFastbootd();
+        EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+    }
+
+    /**
+     * Unit test for {@link NativeDevice#rebootIntoFastbootd()}} when device is already in fastboot
+     * mode.
+     */
+    @Test
+    public void testRebootIntoFastbootd_forceFastboot() throws Exception {
+        TestableAndroidNativeDevice testDevice =
+                new TestableAndroidNativeDevice() {
+                    @Override
+                    public TestDeviceState getDeviceState() {
+                        return TestDeviceState.FASTBOOT;
+                    }
+
+                    @Override
+                    public CommandResult executeFastbootCommand(String... cmdArgs)
+                            throws DeviceNotAvailableException, UnsupportedOperationException {
+                        if (cmdArgs[0].equals("reboot-fastboot")) {
+                            wasCalled = true;
+                        }
+                        return new CommandResult();
+                    }
+                };
+        EasyMock.expect(mMockStateMonitor.waitForDeviceBootloader(EasyMock.anyLong()))
+                .andReturn(true);
+        EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+        testDevice.rebootIntoFastbootd();
+        assertTrue(testDevice.wasCalled);
+        EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
+    }
+
     /** Unit test for {@link NativeDevice#unlockDevice()} already decrypted. */
     @Test
     public void testUnlockDevice_skipping() throws Exception {
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index 7f56715..b0da7d4 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -364,8 +364,8 @@
                 ContentProviderHandler.createEscapedContentUri("filepath/file name spaced (data)");
         // We expect the full url to be quoted to avoid space issues and the URL to be encoded.
         assertEquals(
-                "\"content://android.tradefed.contentprovider/filepath%252Ffile%2520name"
-                        + "%2520spaced%2520%28data%29\"",
+                "\"content://android.tradefed.contentprovider/filepath%252Ffile+name+spaced+"
+                        + "%2528data%2529\"",
                 espacedUrl);
     }
 
diff --git a/tests/src/com/android/tradefed/device/metric/HostStatsdMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/HostStatsdMetricCollectorTest.java
index 8056036..8f13ba9 100644
--- a/tests/src/com/android/tradefed/device/metric/HostStatsdMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/HostStatsdMetricCollectorTest.java
@@ -20,6 +20,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
@@ -31,6 +32,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -43,15 +45,15 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /** Unit Tests for {@link HostStatsdMetricCollector}. */
 @RunWith(JUnit4.class)
 public class HostStatsdMetricCollectorTest {
     private static final String STATSD_CONFIG = "statsd.config";
-    private static final String[] DEVICE_SERIALS = new String[] {"device_1", "device_2"};
     private static final long CONFIG_ID = 54321L;
 
     @Mock private IInvocationContext mContext;
@@ -59,34 +61,58 @@
     @Spy private HostStatsdMetricCollector mCollector;
     @Rule public TemporaryFolder mFolder = new TemporaryFolder();
 
-    @Before
-    public void setUp() throws IOException {
-        initMocks(this);
-    }
+    private TestDescription mTest = new TestDescription("Foo", "Bar");
+    private List<ITestDevice> mDevices =
+            Stream.of("device_1", "device_2").map(this::mockDevice).collect(Collectors.toList());
+    private HashMap<String, Metric> mMetrics = new HashMap<>();
 
-    /** Test that a binary config is pushed and report is dumped from multiple devices. */
-    @Test
-    public void testMetricCollection_binaryConfig_multiDevice()
-            throws IOException, ConfigurationException, DeviceNotAvailableException {
+    @Before
+    public void setUp() throws IOException, ConfigurationException, DeviceNotAvailableException {
+        initMocks(this);
         OptionSetter options = new OptionSetter(mCollector);
         options.setOptionValue(
                 "binary-stats-config", mFolder.newFile(STATSD_CONFIG).getAbsolutePath());
 
-        List<ITestDevice> devices = new ArrayList<>();
-        for (String serial : DEVICE_SERIALS) {
-            devices.add(mockDevice(serial));
-        }
-        when(mContext.getDevices()).thenReturn(devices);
+        when(mContext.getDevices()).thenReturn(mDevices);
         doReturn(CONFIG_ID)
                 .when(mCollector)
                 .pushBinaryStatsConfig(any(ITestDevice.class), any(File.class));
+    }
 
-        HashMap<String, Metric> runMetrics = new HashMap<>();
+    /** Test at per-test level that a binary config is pushed and report is dumped */
+    @Test
+    public void testCollect_perTest()
+            throws IOException, DeviceNotAvailableException, ConfigurationException {
+        OptionSetter options = new OptionSetter(mCollector);
+        options.setOptionValue("per-run", "false");
+
         mCollector.init(mContext, mListener);
-        mCollector.testRunStarted("collect-metrics", 1);
-        mCollector.testRunEnded(0L, runMetrics);
+        mCollector.testRunStarted("collect-metrics", 2);
+        mCollector.testStarted(mTest);
+        mCollector.testEnded(mTest, mMetrics);
+        mCollector.testStarted(mTest);
+        mCollector.testEnded(mTest, mMetrics);
+        mCollector.testRunEnded(0L, mMetrics);
 
-        for (ITestDevice device : devices) {
+        for (ITestDevice device : mDevices) {
+            verify(mCollector, times(2)).pushBinaryStatsConfig(eq(device), any(File.class));
+            verify(mCollector, times(2)).getReportByteStream(eq(device), anyLong());
+            verify(mCollector, times(2)).removeConfig(eq(device), anyLong());
+        }
+    }
+
+    /** Test at per-run level that a binary config is pushed and report is dumped at run level. */
+    @Test
+    public void testCollect_perRun() throws IOException, DeviceNotAvailableException {
+        mCollector.init(mContext, mListener);
+        mCollector.testRunStarted("collect-metrics", 2);
+        mCollector.testStarted(mTest);
+        mCollector.testEnded(mTest, mMetrics);
+        mCollector.testStarted(mTest);
+        mCollector.testEnded(mTest, mMetrics);
+        mCollector.testRunEnded(0L, mMetrics);
+
+        for (ITestDevice device : mDevices) {
             verify(mCollector).pushBinaryStatsConfig(eq(device), any(File.class));
             verify(mCollector).getReportByteStream(eq(device), anyLong());
             verify(mCollector).removeConfig(eq(device), anyLong());
diff --git a/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java b/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java
new file mode 100644
index 0000000..a170e8e
--- /dev/null
+++ b/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 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.tradefed.invoker.logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.command.CommandOptions;
+import com.android.tradefed.targetprep.BaseTargetPreparer;
+import com.android.tradefed.targetprep.TestAppInstallSetup;
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.UUID;
+
+/** Unit tests for {@link TfObjectTracker}. */
+@RunWith(JUnit4.class)
+public class TfObjectTrackerTest {
+
+    @Test
+    public void testNotTracking() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, true, CommandOptions.class);
+        assertTrue(metrics.isEmpty());
+    }
+
+    @Test
+    public void testTrackingWithParents() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, true, SuiteApkInstaller.class);
+        assertEquals(3, metrics.size());
+        assertEquals(1, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertEquals(1, metrics.get(TestAppInstallSetup.class.getName()).longValue());
+        assertEquals(1, metrics.get(BaseTargetPreparer.class.getName()).longValue());
+        // Add a second time
+        metrics = logMetric(testGroup, true, SuiteApkInstaller.class);
+        assertEquals(3, metrics.size());
+        assertEquals(2, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertEquals(2, metrics.get(TestAppInstallSetup.class.getName()).longValue());
+        assertEquals(2, metrics.get(BaseTargetPreparer.class.getName()).longValue());
+    }
+
+    @Test
+    public void testTracking() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, false, SuiteApkInstaller.class);
+        assertEquals(1, metrics.size());
+        assertEquals(1, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertNull(metrics.get(TestAppInstallSetup.class.getName()));
+        assertNull(metrics.get(BaseTargetPreparer.class.getName()));
+        // Add a second time
+        metrics = logMetric(testGroup, false, SuiteApkInstaller.class);
+        assertEquals(1, metrics.size());
+        assertEquals(2, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertNull(metrics.get(TestAppInstallSetup.class.getName()));
+        assertNull(metrics.get(BaseTargetPreparer.class.getName()));
+    }
+
+    private Map<String, Long> logMetric(
+            ThreadGroup testGroup, boolean trackWithParents, Class<?> toTrack) throws Exception {
+        TestRunnable runnable = new TestRunnable(toTrack, trackWithParents);
+        Thread testThread = new Thread(testGroup, runnable);
+        testThread.setName("TfObjectTrackerTest-test-thread");
+        testThread.setDaemon(true);
+        testThread.start();
+        testThread.join(10000);
+        return runnable.getUsageMap();
+    }
+
+    private class TestRunnable implements Runnable {
+
+        private Class<?> mToTrack;
+        private Map<String, Long> mResultMap;
+        private boolean mTrackWithParents;
+
+        public TestRunnable(Class<?> toTrack, boolean trackWithParents) {
+            mToTrack = toTrack;
+            mTrackWithParents = trackWithParents;
+        }
+
+        @Override
+        public void run() {
+            if (mTrackWithParents) {
+                TfObjectTracker.countWithParents(mToTrack);
+            } else {
+                TfObjectTracker.count(mToTrack);
+            }
+            mResultMap = TfObjectTracker.getUsage();
+        }
+
+        public Map<String, Long> getUsageMap() {
+            return mResultMap;
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
new file mode 100644
index 0000000..8656f66
--- /dev/null
+++ b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2020 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.tradefed.postprocessor;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.postprocessor.PerfettoGenericPostProcessor.METRIC_FILE_FORMAT;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.TextFormat.ParseException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import perfetto.protos.PerfettoMergedMetrics.TraceMetrics;
+
+/** Unit tests for {@link PerfettoGenericPostProcessor}. */
+@RunWith(JUnit4.class)
+public class PerfettoGenericPostProcessorTest {
+
+    @Mock private ITestInvocationListener mListener;
+    private PerfettoGenericPostProcessor mProcessor;
+    private OptionSetter mOptionSetter;
+
+    private static final String PREFIX_OPTION = "perfetto-proto-file-prefix";
+    private static final String PREFIX_OPTION_VALUE = "metric-perfetto";
+    private static final String INDEX_OPTION = "perfetto-indexed-list-field";
+    private static final String REGEX_OPTION_VALUE = "perfetto-metric-filter-regex";
+    private static final String ALL_METRICS_OPTION = "perfetto-include-all-metrics";
+    private static final String FILE_FORMAT_OPTION = "trace-processor-output-format";
+
+    File perfettoMetricProtoFile = null;
+
+    @Before
+    public void setUp() throws ConfigurationException {
+        initMocks(this);
+        mProcessor = new PerfettoGenericPostProcessor();
+        mProcessor.init(mListener);
+        mOptionSetter = new OptionSetter(mProcessor);
+    }
+
+    /**
+     * Test metrics count should be zero if "perfetto-include-all-metrics" is not set or set to
+     * false;
+     */
+    @Test
+    public void testNoMetricsByDefault() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(INDEX_OPTION, "perfetto.protos.AndroidStartupMetric.startup");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertTrue(
+                "Number of metrics parsed without indexing is incorrect.",
+                parsedMetrics.size() == 0);
+    }
+
+    /**
+     * Test metrics are filtered correctly when filter regex are passed and
+     * "perfetto-include-all-metrics" is set to false (Note: by default false)
+     */
+    @Test
+    public void testMetricsFilterWithRegEx() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(INDEX_OPTION, "perfetto.protos.AndroidStartupMetric.startup");
+        mOptionSetter.setOptionValue(REGEX_OPTION_VALUE, "android_startup-startup-1.*");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertTrue(
+                "Number of metrics parsed filter regex match is incorrect.",
+                parsedMetrics.size() == 32);
+        assertMetricsContain(parsedMetrics, "android_startup-startup-1-startup_id", 1);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_startup-startup-1-package_name-com.google."
+                        + "android.apps.nexuslauncher-to_first_frame-dur_ns",
+                36175473);
+    }
+
+    /**
+     * Test all metrics are included when "perfetto-include-all-metrics" is set to true and ignores
+     * any of the filter regex set.
+     */
+    @Test
+    public void testAllMetricsOptionIgnoresFilter() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(INDEX_OPTION, "perfetto.protos.AndroidStartupMetric.startup");
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertTrue(
+                "Number of metrics parsed without indexing is incorrect.",
+                parsedMetrics.size() == 76);
+    }
+
+    /** Test that the post processor can parse reports from test metrics. */
+    @Test
+    public void testParsingTestMetrics() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processTestMetricsAndLogs(
+                        new TestDescription("class", "test"), new HashMap<>(), testLogs);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_mem-process_metrics-process_name-"
+                        + ".dataservices-total_counters-anon_rss-min",
+                27938816);
+    }
+
+    /** Test the post processor can parse reports from run metrics. */
+    @Test
+    public void testParsingRunMetrics() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_mem-process_metrics-process_name-"
+                        + ".dataservices-total_counters-anon_rss-min",
+                27938816);
+    }
+
+    /**
+     * Test metrics count and metrics without indexing. In case of app startup metrics startup
+     * messages for same package name will be overridden without indexing.
+     */
+    @Test
+    public void testParsingWithoutIndexing() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertTrue(
+                "Number of metrics parsed without indexing is incorrect.",
+                parsedMetrics.size() == 44);
+        assertMetricsContain(parsedMetrics, "android_startup-startup-startup_id", 2);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_startup-startup-package_name-com.google."
+                        + "android.apps.nexuslauncher-to_first_frame-dur_ns",
+                53102401);
+    }
+
+    /**
+     * Test metrics count and metrics with indexing. In case of app startup metrics, startup
+     * messages for same package name will not be overridden with indexing.
+     */
+    @Test
+    public void testParsingWithIndexing() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(INDEX_OPTION, "perfetto.protos.AndroidStartupMetric.startup");
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertTrue(
+                "Number of metrics parsed with indexing is incorrect.", parsedMetrics.size() == 76);
+        assertMetricsContain(parsedMetrics, "android_startup-startup-1-startup_id", 1);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_startup-startup-1-package_name-com.google."
+                        + "android.apps.nexuslauncher-to_first_frame-dur_ns",
+                36175473);
+        assertMetricsContain(parsedMetrics, "android_startup-startup-2-startup_id", 2);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_startup-startup-2-package_name-com.google."
+                        + "android.apps.nexuslauncher-to_first_frame-dur_ns",
+                53102401);
+    }
+
+    /** Test the post processor can parse binary perfetto metric proto format. */
+    @Test
+    public void testParsingBinaryProto() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.binary, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        mOptionSetter.setOptionValue(FILE_FORMAT_OPTION, "binary");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.PB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_mem-process_metrics-process_name-"
+                        + ".dataservices-total_counters-anon_rss-min",
+                27938816);
+    }
+
+    /** Test the post processor can parse binary perfetto metric proto format. */
+    @Test
+    public void testNoSupportForJsonParsing() throws ConfigurationException, IOException {
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        mOptionSetter.setOptionValue(FILE_FORMAT_OPTION, "json");
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertTrue("Should not have any metrics if json format is set", parsedMetrics.size() == 0);
+    }
+
+    /**
+     * Test the post processor can parse reports from run metrics when the text proto file is
+     * compressed format.
+     */
+    @Test
+    public void testParsingRunMetricsWithCompressedFile()
+            throws ConfigurationException, IOException {
+        // Setup compressed text proto metric file.
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertMetricsContain(
+                parsedMetrics,
+                "android_mem-process_metrics-process_name-"
+                        + ".dataservices-total_counters-anon_rss-min",
+                27938816);
+    }
+
+    /** Creates sample perfetto metric proto file used for testing. */
+    private File setupPerfettoMetricFile(METRIC_FILE_FORMAT format, boolean isCompressed)
+            throws IOException {
+        String perfettoTextContent =
+                "android_mem {\n"
+                        + "  process_metrics {\n"
+                        + "    process_name: \".dataservices\"\n"
+                        + "    total_counters {\n"
+                        + "      anon_rss {\n"
+                        + "        min: 27938816\n"
+                        + "        max: 27938816\n"
+                        + "        avg: 27938816\n"
+                        + "      }\n"
+                        + "      file_rss {\n"
+                        + "        min: 62390272\n"
+                        + "        max: 62390272\n"
+                        + "        avg: 62390272\n"
+                        + "      }\n"
+                        + "      swap {\n"
+                        + "        min: 0\n"
+                        + "        max: 0\n"
+                        + "        avg: 0\n"
+                        + "      }\n"
+                        + "      anon_and_swap {\n"
+                        + "        min: 27938816\n"
+                        + "        max: 27938816\n"
+                        + "        avg: 27938816\n"
+                        + "      }\n"
+                        + "    }\n"
+                        + "}}"
+                        + "android_startup {\n"
+                        + "  startup {\n"
+                        + "    startup_id: 1\n"
+                        + "    package_name: \"com.google.android.apps.nexuslauncher\"\n"
+                        + "    process_name: \"com.google.android.apps.nexuslauncher\"\n"
+                        + "    zygote_new_process: false\n"
+                        + "    to_first_frame {\n"
+                        + "      dur_ns: 36175473\n"
+                        + "      main_thread_by_task_state {\n"
+                        + "        running_dur_ns: 11496200\n"
+                        + "        runnable_dur_ns: 487290\n"
+                        + "        uninterruptible_sleep_dur_ns: 0\n"
+                        + "        interruptible_sleep_dur_ns: 23645107\n"
+                        + "      }\n"
+                        + "      other_processes_spawned_count: 0\n"
+                        + "      time_activity_manager {\n"
+                        + "        dur_ns: 4135001\n"
+                        + "      }\n"
+                        + "      time_activity_resume {\n"
+                        + "        dur_ns: 345105\n"
+                        + "      }\n"
+                        + "      time_choreographer {\n"
+                        + "        dur_ns: 15314324\n"
+                        + "      }\n"
+                        + "      other_process_to_activity_cpu_ratio: 6.9345600857535672\n"
+                        + "    }\n"
+                        + "    activity_hosting_process_count: 1\n"
+                        + "  }\n"
+                        + "  startup {\n"
+                        + "    startup_id: 2\n"
+                        + "    package_name: \"com.google.android.apps.nexuslauncher\"\n"
+                        + "    process_name: \"com.google.android.apps.nexuslauncher\"\n"
+                        + "    zygote_new_process: false\n"
+                        + "    to_first_frame {\n"
+                        + "      dur_ns: 53102401\n"
+                        + "      main_thread_by_task_state {\n"
+                        + "        running_dur_ns: 9766774\n"
+                        + "        runnable_dur_ns: 320103\n"
+                        + "        uninterruptible_sleep_dur_ns: 0\n"
+                        + "        interruptible_sleep_dur_ns: 42358858\n"
+                        + "      }\n"
+                        + "      other_processes_spawned_count: 0\n"
+                        + "      time_activity_manager {\n"
+                        + "        dur_ns: 4742396\n"
+                        + "      }\n"
+                        + "      time_activity_resume {\n"
+                        + "        dur_ns: 280208\n"
+                        + "      }\n"
+                        + "      time_choreographer {\n"
+                        + "        dur_ns: 13705366\n"
+                        + "      }\n"
+                        + "      other_process_to_activity_cpu_ratio: 12.956123015968883\n"
+                        + "    }\n"
+                        + "    activity_hosting_process_count: 1\n"
+                        + "  }\n"
+                        + "}";
+        FileWriter fileWriter = null;
+        try {
+            perfettoMetricProtoFile = FileUtil.createTempFile("metric_perfetto", "");
+            fileWriter = new FileWriter(perfettoMetricProtoFile);
+            fileWriter.write(perfettoTextContent);
+        } finally {
+            if (fileWriter != null) {
+                fileWriter.close();
+            }
+        }
+
+        if (format.equals(METRIC_FILE_FORMAT.binary)) {
+            File perfettoBinaryFile = FileUtil.createTempFile("metric_perfetto_binary", ".pb");
+            try (BufferedReader bufferedReader =
+                    new BufferedReader(new FileReader(perfettoMetricProtoFile))) {
+                TraceMetrics.Builder builder = TraceMetrics.newBuilder();
+                TextFormat.merge(bufferedReader, builder);
+                builder.build().writeTo(new FileOutputStream(perfettoBinaryFile));
+            } catch (ParseException e) {
+                CLog.e("Failed to merge the perfetto metric file." + e.getMessage());
+            } catch (IOException ioe) {
+                CLog.e(
+                        "IOException happened when reading the perfetto metric file."
+                                + ioe.getMessage());
+            } finally {
+                perfettoMetricProtoFile.delete();
+                perfettoMetricProtoFile = perfettoBinaryFile;
+            }
+            return perfettoMetricProtoFile;
+        }
+
+        if (isCompressed) {
+            perfettoMetricProtoFile = compressFile(perfettoMetricProtoFile);
+        }
+        return perfettoMetricProtoFile;
+    }
+
+    /** Create a zip file with perfetto metric proto file */
+    private File compressFile(File decompressedFile) throws IOException {
+        File compressedFile = FileUtil.createTempFile("compressed_temp", ".zip");
+        try {
+            ZipUtil.createZip(decompressedFile, compressedFile);
+        } catch (IOException ioe) {
+            CLog.e("Unable to gzip the file.");
+        } finally {
+            decompressedFile.delete();
+        }
+        return compressedFile;
+    }
+
+    @After
+    public void teardown() {
+        if (perfettoMetricProtoFile != null) {
+            perfettoMetricProtoFile.delete();
+        }
+    }
+
+    /** Assert that metrics contain a key and a corresponding value. */
+    private void assertMetricsContain(
+            Map<String, Metric.Builder> metrics, String key, Object value) {
+        assertTrue(
+                String.format(
+                        "Metric with key containing %s and value %s was expected but not found.",
+                        key, value),
+                metrics.entrySet()
+                        .stream()
+                        .anyMatch(
+                                e ->
+                                        e.getKey().contains(key)
+                                                && (String.valueOf(value)
+                                                        .equals(
+                                                                e.getValue()
+                                                                        .build()
+                                                                        .getMeasurements()
+                                                                        .getSingleString()))));
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/AllTestAppsInstallSetupTest.java b/tests/src/com/android/tradefed/targetprep/AllTestAppsInstallSetupTest.java
index fa9673f..888afd1 100644
--- a/tests/src/com/android/tradefed/targetprep/AllTestAppsInstallSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/AllTestAppsInstallSetupTest.java
@@ -33,6 +33,7 @@
         mMockTestDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockTestDevice.getDeviceDescriptor()).andStubReturn(null);
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andStubReturn(false);
     }
 
     public void testNotIDeviceBuildInfo() throws DeviceNotAvailableException {
@@ -93,6 +94,22 @@
             FileUtil.recursiveDelete(testDir);
         }
     }
+    public void testSetupForceQueryable() throws Exception {
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andReturn(true);
+        File testDir = FileUtil.createTempDir("TestAppSetupForceQueryableTest");
+        // fake hierarchy of directory and files
+        FileUtil.createTempFile("fakeApk", ".apk", testDir);
+        try {
+            EasyMock.expect(mMockBuildInfo.getTestsDir()).andReturn(testDir);
+            EasyMock.expect(mMockTestDevice.installPackage((File)EasyMock.anyObject(),
+                    EasyMock.eq(true), EasyMock.eq("--force-queryable"))).andReturn(null);
+            EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+            mPrep.setUp(mMockTestDevice, mMockBuildInfo);
+            EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+        } finally {
+            FileUtil.recursiveDelete(testDir);
+        }
+    }
 
     public void testInstallFailure() throws DeviceNotAvailableException {
         final String failure = "INSTALL_PARSE_FAILED_MANIFEST_MALFORMED";
diff --git a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
index 870fadf..921c0cc 100644
--- a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
@@ -38,7 +38,6 @@
 import org.mockito.Mockito;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -59,11 +58,12 @@
     private List<VersionedFile> mApps;
 
     @Before
-    public void setUp() throws IOException {
+    public void setUp() throws Exception {
         mAppSetup = new AppSetup();
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockDevice.getDeviceDescriptor()).andStubReturn(null);
+        EasyMock.expect(mMockDevice.isAppEnumerationSupported()).andStubReturn(false);
         mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
         mMockAaptParser = Mockito.mock(AaptParser.class);
         mApps = new ArrayList<>();
diff --git a/tests/src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetupTest.java b/tests/src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetupTest.java
index 46dda76..ff1f7d1 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetupTest.java
@@ -76,6 +76,7 @@
         mMockTestDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockTestDevice.getDeviceDescriptor()).andStubReturn(null);
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andStubReturn(false);
     }
 
     @After
@@ -153,6 +154,28 @@
     }
 
     @Test
+    public void testForceQueryableSuccess() throws Exception {
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andReturn(true);
+        mPrep.setTestZipName("zip");
+
+        mMockBuildInfo.getFile((String) EasyMock.anyObject());
+        EasyMock.expectLastCall().andReturn(new File("zip"));
+
+        setMockUnzipDir();
+        mMockTestDevice.installPackage(
+                EasyMock.anyObject(), EasyMock.anyBoolean(), EasyMock.eq("--force-queryable"));
+        EasyMock.expectLastCall().andReturn(null).times(3);
+        mMockTestDevice.uninstallPackage((String) EasyMock.anyObject());
+        EasyMock.expectLastCall().andReturn(null).times(3);
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+
+        mPrep.setUp(mMockTestDevice, mMockBuildInfo);
+        mPrep.tearDown(mMockTestDevice, mMockBuildInfo, null);
+
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+    }
+
+    @Test
     public void testSuccessNoTearDown() throws Exception {
         mPrep.setTestZipName("zip");
         mPrep.setCleanup(false);
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApkSetupTest.java b/tests/src/com/android/tradefed/targetprep/InstallApkSetupTest.java
index 8d92719..7c9f0f1 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApkSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApkSetupTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.tradefed.targetprep;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -31,8 +34,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 
-import static org.junit.Assert.*;
-
 /**
  * Unit tests for {@link InstallApkSetup}
  */
@@ -55,6 +56,7 @@
         mMockTestDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockTestDevice.getDeviceDescriptor()).andStubReturn(null);
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andStubReturn(false);
 
         testDir = FileUtil.createTempDir("TestApkDir");
         testFile = FileUtil.createTempFile("File", ".apk", testDir);
@@ -82,6 +84,24 @@
     }
 
     /**
+     * Test {@link InstallApkSetupTest#setUp()} by successfully installing 2 Apk files
+     */
+    @Test
+    public void testSetupForceQueryable()
+            throws DeviceNotAvailableException, BuildError, TargetSetupError {
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andReturn(true);
+
+        testCollectionFiles.add(testFile);
+        testCollectionFiles.add(testFile);
+        mInstallApkSetup.setApkPaths(testCollectionFiles);
+        EasyMock.expect(mMockTestDevice.installPackage((File) EasyMock.anyObject(),
+                EasyMock.eq(true), EasyMock.eq("--force-queryable"))).andReturn(null).times(2);
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+        mInstallApkSetup.setUp(mMockTestDevice, mMockBuildInfo);
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+    }
+
+    /**
      * Test {@link InstallApkSetupTest#setUp()} by installing a non-existing Apk
      */
     @Test
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index c8ca2e6..4ab06a7 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -30,6 +30,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.testtype.Abi;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.FileUtil;
@@ -53,6 +54,7 @@
     private PushFilePreparer mPreparer = null;
     private ITestDevice mMockDevice = null;
     private OptionSetter mOptionSetter = null;
+    private TestInformation mTestInfo;
 
     @Before
     public void setUp() throws Exception {
@@ -61,13 +63,16 @@
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("SERIAL");
         mPreparer = new PushFilePreparer();
         mOptionSetter = new OptionSetter(mPreparer);
+        IInvocationContext context = new InvocationContext();
+        context.addAllocatedDevice("device", mMockDevice);
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
     }
 
     /** When there's nothing to be done, expect no exception to be thrown */
     @Test
     public void testNoop() throws Exception {
         EasyMock.replay(mMockDevice);
-        mPreparer.setUp(mMockDevice, null);
+        mPreparer.setUp(mTestInfo);
         EasyMock.verify(mMockDevice);
     }
 
@@ -77,8 +82,9 @@
         mOptionSetter.setOptionValue("post-push", "ls /");
         EasyMock.replay(mMockDevice);
         try {
+            mTestInfo.getContext().addDeviceBuildInfo("device", new BuildInfo());
             // Should throw TargetSetupError and _not_ run any post-push command
-            mPreparer.setUp(mMockDevice, null);
+            mPreparer.setUp(mTestInfo);
             fail("TargetSetupError not thrown");
         } catch (TargetSetupError e) {
             // expected
@@ -96,8 +102,9 @@
                 .andReturn(Boolean.FALSE);
         EasyMock.replay(mMockDevice);
         try {
+            mTestInfo.getContext().addDeviceBuildInfo("device", new BuildInfo());
             // Should throw TargetSetupError and _not_ run any post-push command
-            mPreparer.setUp(mMockDevice, null);
+            mPreparer.setUp(mTestInfo);
             fail("TargetSetupError not thrown");
         } catch (TargetSetupError e) {
             // expected
@@ -123,8 +130,9 @@
                             mMockDevice.pushFile(
                                     EasyMock.eq(testFile), EasyMock.eq("/data/local/tmp/")))
                     .andReturn(Boolean.TRUE);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(testsDir);
@@ -150,8 +158,9 @@
                                     EasyMock.eq("/data/local/tmp/"),
                                     EasyMock.anyObject()))
                     .andReturn(Boolean.TRUE);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(testsDir);
@@ -171,8 +180,9 @@
             EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/file")).andReturn(true);
             EasyMock.expect(mMockDevice.isDirectory("/data/local/tmp/file")).andReturn(false);
             EasyMock.replay(mMockDevice);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             try {
-                mPreparer.setUp(mMockDevice, info);
+                mPreparer.setUp(mTestInfo);
                 fail("Should have thrown an exception.");
             } catch (TargetSetupError expected) {
                 // Expected
@@ -207,8 +217,9 @@
                                     EasyMock.eq(testFile2),
                                     EasyMock.eq("/data/local/tmp/perf_test")))
                     .andReturn(Boolean.TRUE);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(testsDir);
@@ -240,8 +251,9 @@
                                     EasyMock.eq(testFile2),
                                     EasyMock.eq("/data/local/tmp/perf_test")))
                     .andReturn(Boolean.TRUE);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(testsDir);
@@ -261,9 +273,9 @@
         // Because we're only warning, the post-push command should be run despite the push failures
         EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("ls /"))).andReturn("");
         EasyMock.replay(mMockDevice);
-
+        mTestInfo.getContext().addDeviceBuildInfo("device", new BuildInfo());
         // Don't expect any exceptions to be thrown
-        mPreparer.setUp(mMockDevice, null);
+        mPreparer.setUp(mTestInfo);
         EasyMock.verify(mMockDevice);
     }
 
@@ -374,9 +386,9 @@
                                     EasyMock.eq("/data/local/tmp/debugger"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
-
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -411,9 +423,9 @@
                                     EasyMock.eq("/data/local/tmp/folder"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
-
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -446,8 +458,9 @@
                                     EasyMock.eq("/data/local/tmp/debugger"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -487,8 +500,9 @@
                                     EasyMock.eq("/data/local/tmp/lib"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -525,8 +539,9 @@
                                                     "target/testcases/aaaaa/x86_64/file")),
                                     EasyMock.eq("/data/local/tmp/file")))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(tmpFolder);
@@ -562,8 +577,9 @@
                                     EasyMock.eq("/data/local/tmp/lib"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -600,8 +616,9 @@
                                     EasyMock.eq("/data/local/tmp/lib"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -643,8 +660,9 @@
                                     EasyMock.eq("/data/local/tmp/debugger"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -682,8 +700,9 @@
                                     EasyMock.eq("/data/local/tmp/lib"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -756,8 +775,9 @@
                                     EasyMock.eq("/data/local/tmp/propertyinfoserializer_tests"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
@@ -793,8 +813,9 @@
                                     EasyMock.eq("/data/local/tmp/lib"),
                                     EasyMock.capture(capture)))
                     .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
             EasyMock.replay(mMockDevice);
-            mPreparer.setUp(mMockDevice, info);
+            mPreparer.setUp(mTestInfo);
             EasyMock.verify(mMockDevice);
             // The x86 folder was not filtered
             Set<String> capValue = capture.getValue();
diff --git a/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java b/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
index ecd64da..258dd7a 100644
--- a/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
@@ -100,6 +100,7 @@
         mMockTestDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockTestDevice.getDeviceDescriptor()).andStubReturn(null);
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andStubReturn(false);
         IInvocationContext context = new InvocationContext();
         context.addAllocatedDevice("device", mMockTestDevice);
         context.addDeviceBuildInfo("device", mMockBuildInfo);
@@ -246,6 +247,25 @@
     }
 
     @Test
+    public void testSetup_forceQueryable() throws Exception {
+        EasyMock.expect(mMockTestDevice.isAppEnumerationSupported()).andReturn(true);
+
+        OptionSetter setter = new OptionSetter(mPrep);
+
+        EasyMock.expect(mMockTestDevice.installPackage(
+                EasyMock.eq(fakeApk), EasyMock.eq(true), EasyMock.eq("--force-queryable")))
+                .andReturn(null);
+        EasyMock.expect(
+                mMockTestDevice.installPackages(
+                        EasyMock.eq(mTestSplitApkFiles), EasyMock.eq(true),
+                        EasyMock.eq("--force-queryable")))
+                .andReturn(null);
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+        mPrep.setUp(mTestInfo);
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+    }
+
+    @Test
     public void testInstallFailure() throws Exception {
         final String failure = "INSTALL_PARSE_FAILED_MANIFEST_MALFORMED";
         EasyMock.expect(mMockTestDevice.installPackage(EasyMock.eq(fakeApk), EasyMock.eq(true)))
@@ -293,13 +313,14 @@
     @Test
     public void testMissingApk() throws Exception {
         fakeApk = null; // Apk doesn't exist
-
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
         try {
             mPrep.setUp(mTestInfo);
             fail("TestAppInstallSetup#setUp() did not raise TargetSetupError with missing apk.");
         } catch (TargetSetupError e) {
             assertTrue(e.getMessage().contains("not found"));
         }
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
     }
 
     /**
@@ -309,13 +330,14 @@
     @Test
     public void testUnreadableApk() throws Exception {
         fakeApk = new File("/not/a/real/path"); // Apk cannot be read
-
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
         try {
             mPrep.setUp(mTestInfo);
             fail("TestAppInstallSetup#setUp() did not raise TargetSetupError with unreadable apk.");
         } catch (TargetSetupError e) {
             assertTrue(e.getMessage().contains("not read"));
         }
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
     }
 
     /**
@@ -326,8 +348,9 @@
     public void testMissingApk_silent() throws Exception {
         fakeApk = null; // Apk doesn't exist
         mSetter.setOptionValue("throw-if-not-found", "false");
-
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
         mPrep.setUp(mTestInfo);
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
     }
 
     /**
@@ -338,8 +361,9 @@
     public void testUnreadableApk_silent() throws Exception {
         fakeApk = new File("/not/a/real/path"); // Apk cannot be read
         mSetter.setOptionValue("throw-if-not-found", "false");
-
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
         mPrep.setUp(mTestInfo);
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
index 244b052..2ed02a2 100644
--- a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
+++ b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
@@ -134,6 +134,7 @@
         mMockListener = EasyMock.createMock(ITestInvocationListener.class);
         mMockBuild = EasyMock.createMock(IBuildInfo.class);
         mMockDevice = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mMockDevice.isAppEnumerationSupported()).andStubReturn(false);
         mMockContext = new InvocationContext();
         mMockContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
         mMockContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuild);
diff --git a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
index 0e6a655..5f23f21 100644
--- a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
+++ b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
@@ -75,6 +75,23 @@
         }
     }
 
+    /**
+     * '+' character is special in URLs as it can be decoded incorrectly as a space. Ensure it works
+     * and our encoding/decoding handles it well.
+     */
+    @Test
+    public void testPushFile_encode_plus() throws Exception {
+        // Name with '+'
+        File tmpFile = FileUtil.createTempFile("tmpFileToPush+(test)", ".txt");
+        try {
+            boolean res = mHandler.pushFile(tmpFile, "/sdcard/" + tmpFile.getName());
+            assertTrue(res);
+            assertTrue(getDevice().doesFileExist(mCurrentUserStoragePath + tmpFile.getName()));
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
+
     @Test
     public void testPushFile() throws Exception {
         File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");